Compare commits

...

337 Commits

Author SHA1 Message Date
JT 3d9c193e02
Merge pull request #629 from LykosAI/main 6 months ago
JT ad79d6e13a
Merge branch 'LykosAI:main' into main 6 months ago
JT 72917d0163
Merge pull request #633 from ionite34/fix-typo 6 months ago
JT 72a3b9163b
Update OpenArtBrowserPage HorizontalAlignment -> HorizontalContentAlignment 6 months ago
JT 9f1c3d5623
Merge pull request #630 from ionite34/bugfixes 6 months ago
JT 4b86f28bb8 Some bugfixes from dev 6 months ago
JT bf749904a5 Merge branch 'main' of https://github.com/ionite34/StabilityMatrix 6 months ago
Ionite b199f5e5fd
Merge pull request #629 from ionite34/backport/main/pr-628 6 months ago
Ionite 8a202c7aef
Update CHANGELOG.md 6 months ago
Ionite 93a16cbd3e
Remove preview torch from mps install 6 months ago
JT d7f977fb86
Merge pull request #613 from notlet/patch-1 7 months ago
let 11fdbdfeed
Add AUR link to README.md 7 months ago
JT d544351dd2
Merge pull request #605 from LykosAI/main 7 months ago
JT 0ede21d512 Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 7 months ago
JT 5c838e9f98 Merge branch 'main' of https://github.com/ionite34/StabilityMatrix 7 months ago
JT c7b638182f
Merge pull request #617 from ionite34/release 7 months ago
JT 7cea793c18
Merge branch 'main' into release 7 months ago
JT 9e530a00b6
Merge pull request #616 from ionite34/fix-kohyaa 7 months ago
JT 9b13cb5a76 undo 7 months ago
JT 4524b9c15b chagenlog 7 months ago
JT aca2e7a767 Fix kohya launch not working & fix wrong package name in breadcrumb 7 months ago
JT 52597ad675
Merge branch 'LykosAI:main' into main 7 months ago
JT 9fd20a7dd6 Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 7 months ago
JT e1a9202cf7 Merge branch 'main' of https://github.com/ionite34/StabilityMatrix 7 months ago
JT 5eaceaac37
Merge pull request #613 from ionite34/2.10.2 7 months ago
JT ae82e598a6
Merge pull request #615 from ionite34/dev 7 months ago
JT 56f5dca3bb
Merge pull request #614 from ionite34/new-roles 7 months ago
JT 7e436ba132 new roles 7 months ago
JT f06b9ef077
Merge branch 'main' into 2.10.2 7 months ago
JT 919a013f1b
Merge pull request #611 from ionite34/translation-updates 7 months ago
JT 81cf470e2d
Merge branch 'dev' into translation-updates 7 months ago
JT 14b3bd191e Updated spanish and turkish translations from poeditor 7 months ago
JT 9ae41313aa
Merge pull request #610 from ionite34/mac-fixes 7 months ago
JT 047d973b11 use AvaloniaVersion variable 7 months ago
jt d543b1a36e Fix some crashes on macOS & linux 7 months ago
JT e3df430df5
Merge pull request #609 from ionite34/comment-parsing 7 months ago
JT 1e1cb10940 Merge branch 'comment-parsing' of https://github.com/ionite34/StabilityMatrix into comment-parsing 7 months ago
JT c66c4972b8 no empty strings 7 months ago
JT 7b962dbae5
Merge branch 'dev' into comment-parsing 7 months ago
JT 2de9fdcdf6 more better comment parsing 7 months ago
JT 153f89ed87
Merge pull request #608 from ionite34/stuff-n-junk 7 months ago
JT db777eeb3c fix comment parsing & metadata parsing 7 months ago
JT c18e93271e
Merge pull request #588 from LykosAI/macOS-build-fix 7 months ago
JT 61c229da44
Update macOS build agent 7 months ago
JT 10d468f455
Merge pull request #587 from LykosAI/main 7 months ago
JT 72394aa1ae
Merge branch 'LykosAI:main' into main 7 months ago
JT 805d3f61e6 Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 7 months ago
JT 3b8b8cc7d5 Merge branch 'main' of https://github.com/ionite34/StabilityMatrix 7 months ago
JT e626c47305
Merge pull request #607 from ionite34/dev 7 months ago
JT d73ca680ab
Merge pull request #606 from ionite34/go-back-after-install 7 months ago
JT fa458e2866
Merge branch 'dev' into go-back-after-install 7 months ago
JT 041ec59914 if the user never closes the package install dialog / is still on the same page when the install is finished, go back to the packages page 7 months ago
JT c285a0c110
Merge pull request #605 from ionite34/moar-bug-fixes 7 months ago
JT a8b4d680a1 Remove update confirmation dialog 7 months ago
JT f86576c9d8 Merge branch 'moar-bug-fixes' of https://github.com/ionite34/StabilityMatrix into moar-bug-fixes 7 months ago
JT aa032e01b9 actually do the thing 7 months ago
JT dbd9efb311
Merge branch 'dev' into moar-bug-fixes 7 months ago
JT 805dde5bd5 fixed a few more github issues 7 months ago
Ionite 9843b6f7ae
Merge pull request #604 from ionite34/fix-crash-when-no-image 7 months ago
JT d441ea94ae moar startswith 7 months ago
JT f97cabc755 Fix crash when trying to add `file://` to an `avares://` uri 7 months ago
JT a66063b031
Merge pull request #603 from ionite34/downmerge 7 months ago
JT 921a54012e
Merge branch 'dev' into downmerge 7 months ago
JT 42545f03f3
Merge pull request #602 from ionite34/actually-use-env-vars 7 months ago
JT e28a3980bf undo settings change 7 months ago
JT f2dde2a50e Actually use the environment variables during install & add pip cache clear button 7 months ago
JT 708743df47
Merge pull request #601 from ionite34/fix-images 7 months ago
jt 848468d525 fix missing images on macos & linux 7 months ago
JT 662022e191
Merge pull request #599 from ionite34/fix-teaching-tip-spam 7 months ago
JT d46b65f3d3 Merge branch 'fix-teaching-tip-spam' of https://github.com/ionite34/StabilityMatrix into fix-teaching-tip-spam 7 months ago
JT 68a9968794 twoway? 7 months ago
Ionite a652287baf
Merge pull request #600 from ionite34/fix-lykos-auth 7 months ago
Ionite 64c55ec379
Update CHANGELOG.md 7 months ago
Ionite 128ef13229
Update CHANGELOG.md 7 months ago
Ionite 0273d8dd3c
Merge branch 'dev' into fix-lykos-auth 7 months ago
Ionite 68647f391e
Merge pull request #598 from ionite34/fix-settings-env 7 months ago
Ionite 01cc2f89a7
Add LykosLoginDialog max width 7 months ago
Ionite a75908e4ef
Improve login error message 7 months ago
Ionite d3b37229da
Fix token refresh when access token is empty 7 months ago
Ionite 90e815a421
Fix auth header added to all lykosauthapi routes 7 months ago
JT fa1c0a403f
Merge branch 'main' into fix-teaching-tip-spam 7 months ago
JT ef25d5b944 Fix Inference teaching tip showing more often & fix `--launch-package` argument not working 7 months ago
Ionite e41b4195dc
Update CHANGELOG.md 7 months ago
Ionite 49aa554257
Fix settings env var conflict 7 months ago
JT b95bf2cabd
Merge pull request #570 from LykosAI/main 7 months ago
JT c029cc9660
Merge branch 'LykosAI:main' into main 7 months ago
JT 9ac2b91b21 Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 7 months ago
JT 5d42c7f9d6 Merge branch 'main' of https://github.com/ionite34/StabilityMatrix 7 months ago
JT 56acf60ead
Merge pull request #594 from ionite34/v2.10 7 months ago
JT 2f89eeba4d
Merge pull request #597 from ionite34/dev 7 months ago
Ionite c2efe33c91
Merge pull request #596 from ionite34/fix-asyncimage 7 months ago
Ionite 2d961be0f3
Remove not needed AsyncImage fix 7 months ago
Ionite b21e11c0c8
Add control theme for BetterAsyncImage 7 months ago
Ionite 224b008cd9
Switch usages to BetterAsyncImage 7 months ago
Ionite 38a104146f
Add vendored labs controls to fix asyncimage 7 months ago
Ionite 94ff2fa40b
Merge pull request #595 from ionite34/inference-tips 7 months ago
Ionite 04e5459f75
Add layer diffuse changelog 7 months ago
Ionite 8e1c00312c
Add Inference prompt help tooltip 7 months ago
JT 2a22a43cc8
Fixed double 2.9.2 changelog 7 months ago
JT 92ddc09d07
Merge branch 'main' into v2.10 7 months ago
JT 795413780c
Merge pull request #593 from ionite34/chagenlogg 7 months ago
JT 8ba15c73a1 oops wrong section 7 months ago
JT 96f507619c release changelog 7 months ago
JT 07af1c85d5
Merge pull request #592 from ionite34/fix-installed-sort 7 months ago
JT 7bf3a7b4ac
Merge branch 'dev' into fix-installed-sort 7 months ago
JT d211db88ab Chunk installed model requests with more than 100 installed models cuz Civit doesn't paginate that 7 months ago
Ionite 9716ae2bf2
Merge pull request #591 from ionite34/fix-setuptools-distutils 7 months ago
Ionite 43f3c28b9a
Update CHANGELOG.md 7 months ago
Ionite 4e42eee757
Add default env vars, set SETUPTOOLS_USE_DISTUTILS=stdlib 7 months ago
JT 60ab7bb3a7
Merge pull request #590 from ionite34/fix-stuffs 7 months ago
JT 5441572e38
Merge branch 'dev' into fix-stuffs 7 months ago
JT 612509f4ff Fixed incorrect output path for A3WebUI & made package launch update output links 7 months ago
JT c8ab3a743e
Merge pull request #589 from ionite34/update-translation 7 months ago
JT 9b0e1b90f5
Merge branch 'dev' into update-translation 7 months ago
JT e5d215463e updated spanish/french/turkish translations 7 months ago
JT 3d1090573b
Merge pull request #588 from ionite34/fix-model-images 7 months ago
JT 026b9d0cb1 Only download images from civitai with Type == "image" since they finally added that property 8 months ago
JT 9c3e80cb4f
Merge pull request #558 from LykosAI/main 8 months ago
JT 9b193f9cb5
Merge branch 'main' into main 8 months ago
JT 2148628aaa Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 8 months ago
JT 773a3a0c1c Merge branch 'main' of https://github.com/ionite34/StabilityMatrix 8 months ago
JT 9937b1ec6e
Merge pull request #587 from ionite34/backport/main/pr-586 8 months ago
Ionite c6a8722ab2 Fix dmg build script 8 months ago
Ionite df3a602e3a
Merge pull request #586 from ionite34/fix-macos-build 8 months ago
Ionite 2d95adbda7
Fix dmg build script 8 months ago
JT 5d43a8df18
Merge pull request #557 from LykosAI/main 8 months ago
Ionite 6bb8cec4b8
Merge pull request #584 from ionite34/inference-layer-diffuse 8 months ago
Ionite 660dad4245
Add layer diffuse selection for SD1.5 / SDXL 8 months ago
JT 77a60b1a59
Merge branch 'LykosAI:main' into main 8 months ago
JT 727b04ca01 Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 8 months ago
Ionite 6621a8ce58
Update CHANGELOG.md 8 months ago
Ionite ff26180e8b
Apply post install dep updates for ComfyUI ext update 8 months ago
JT f224c77349
Merge pull request #585 from ionite34/fix-invoke-and-civit 8 months ago
JT 67ce55d429 Fixed invoke install/run failing & fixed model browser not showing any images 8 months ago
Ionite ae2c9b2783
Merge branch 'inference-layer-diffuse' of https://github.com/ionite34/StabilityMatrix into inference-layer-diffuse 8 months ago
Ionite 395d9c3963
Add PreOutputAction support for Text2Img and Img2Img 8 months ago
Ionite 3161a009c7
Merge branch 'dev' into inference-layer-diffuse 8 months ago
Ionite d6920be164
Add Nodes for LayerDiffuse 8 months ago
Ionite 255b1feb66
Add LayerDiffuseMode 8 months ago
Ionite 52b32f9227
Add LayerDiffuseModule as Sampler addon option 8 months ago
Ionite 51b6e1be36
Add LayerDiffuseCard ViewModel 8 months ago
Ionite cefb08e4da
Add LayerDiffuseModule 8 months ago
Ionite 92d42b3c53
PreOutputActions support for ModuleApplyStepEventArgs 8 months ago
Ionite af89e07c01
Add LayerDiffuseCard 8 months ago
Ionite d37a2df700
Merge pull request #578 from ionite34/registry-perms-fix 8 months ago
Ionite 4058742cd9
Merge branch 'dev' into registry-perms-fix 8 months ago
JT 5d6638334a Merge branch 'main' of https://github.com/ionite34/StabilityMatrix 8 months ago
JT 9d99c59747
Merge pull request #583 from ionite34/some-fixes 8 months ago
JT 61abe7a1f6 use same nsfwlevel 8 months ago
JT 04db64fcfb many change 8 months ago
JT 5419e7a53b
Merge pull request #582 from ionite34/downmerge 8 months ago
JT ca67f73ff6
Fix duplicate InstallerSortOrder 8 months ago
JT c7c5653ff3
Merge branch 'dev' into downmerge 8 months ago
JT 26c9db8b14
Merge pull request #581 from ionite34/some-fixes 8 months ago
JT 772d93d46d
Merge branch 'dev' into some-fixes 8 months ago
JT 870aa20a80 output sharing by default & invokeai fixes & some torch version updates 8 months ago
JT 72f4cf04a7
Merge pull request #567 from ionite34/headless-kohya 8 months ago
JT 0409e0354d
Merge branch 'dev' into headless-kohya 8 months ago
JT 864391db97
Merge pull request #580 from ionite34/async-image-and-nugets 8 months ago
JT 597c4c9aec jsonignore uri & remove unused event handler 8 months ago
JT 035d54c844 remove console write 8 months ago
JT 73771a29c1 change PackageInstallDetailView back to BetterAdvancedImage since the image should be cached from the previous page 8 months ago
JT 3af08e04f3 more chagenlog 8 months ago
JT b2a76056af phrasing 8 months ago
JT f40f0b6cc0 use AsyncImage where possible (some weird stacklayout bug means we still use BetterAdvancedImage in some places). also updated nugets including avalonia to 11.0.10. also updated readme. 8 months ago
JT 7b01968b00
Merge pull request #520 from ionite34/openart 8 months ago
JT f30181caa8 allow download without comfy 8 months ago
JT c217189ade changed how metadata is downloaded & added context menus & localization stuff 8 months ago
JT 8d59e1d9f2 fix merge shenanigans 8 months ago
JT d2ceefdd19
Merge branch 'dev' into openart 8 months ago
JT 4fe73777e6
Merge pull request #579 from ionite34/readme 8 months ago
JT d36f7c8c4a
Update README.md 8 months ago
Ionite 565cfa918b
Use explicit writeable flag for UriHandler registry access 8 months ago
JT c8a2afec28
Merge pull request #531 from LykosAI/main 8 months ago
JT 2b5db18871
Merge branch 'main' into main 8 months ago
JT acca9e685b Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 8 months ago
JT 29b6863943
Merge pull request #577 from ionite34/use-dotnet8-for-pupnet 8 months ago
JT b913e45d5c Use .net 8 for latest pupnet 8 months ago
JT 9ca69298d5
Merge pull request #530 from LykosAI/main 8 months ago
JT d31ae40b2d
Merge branch 'LykosAI:main' into main 8 months ago
JT fcca600fba Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 8 months ago
JT 233eba2fce
Merge pull request #576 from ionite34/moar-backport 8 months ago
JT 4393c01899 fix chagenlog 8 months ago
JT cbc37fb466 backport some bug fixes from dev 8 months ago
JT 0eb88e2255
Merge pull request #575 from ionite34/model-browser-fixes 8 months ago
JT 4846d69043 Merge branch 'model-browser-fixes' of https://github.com/ionite34/StabilityMatrix into model-browser-fixes 8 months ago
JT 19cd60c195 Fixed all that I can fix 8 months ago
JT 078a471eaa Merge branch 'main' of https://github.com/ionite34/StabilityMatrix 8 months ago
JT db239e7c2e
Merge branch 'dev' into model-browser-fixes 8 months ago
JT 6657cf59ea Converted civitai browser to new pagination cursor stuff & converted backend to use DynamicData 8 months ago
JT 8810e18ef6
Merge pull request #574 from ionite34/backport/main/pr-573 8 months ago
JT 76cf8fbd41 Fix star ratings for civitai stuff 8 months ago
JT 50b142ba54
Merge pull request #573 from ionite34/fix-stars 8 months ago
JT 38013c16d6 Fix star ratings for civitai stuff 8 months ago
JT d01cc2aeb4
Merge pull request #572 from ionite34/handle-extension-links 8 months ago
JT ec4c7343da i18n 8 months ago
JT 86bcdaf86f show dialog if another instance is running & fix design view hogging the pipe 8 months ago
JT a5beaf434c handle download links from sm chrome extension & fix model delete not deleting from cache 8 months ago
Ionite 1779da0210
Merge pull request #570 from ionite34/fix-inference-inherit-addons 8 months ago
Ionite 479aceebb0
Fix double chagenlog 8 months ago
Ionite b74ee76130
Merge branch 'dev' into fix-inference-inherit-addons 8 months ago
JT 662c575fe8
Merge pull request #569 from ionite34/one-click-fixes 8 months ago
Ionite c119a3be91
Merge branch 'dev' into fix-inference-inherit-addons 8 months ago
JT 87828cb172
Merge branch 'dev' into one-click-fixes 8 months ago
JT 9c78ef2197
Merge pull request #568 from ionite34/fix-vae-download-folder 8 months ago
Ionite 8503e937ed
Update CHANGELOG.md 8 months ago
Ionite c028a51d71
Fix Inference Inherit Primary Sampler Addons not able to disable 8 months ago
JT 243115b459 one more chagenlog 8 months ago
JT 2f0e081e90 - Fixed crash on startup when library directory is not set 8 months ago
JT 977219995d chagenlog 8 months ago
JT 3c23d6774e Fix model download location for vae when part of a checkpoint CivitModel 8 months ago
JT b28aef1e7b Update for headless kohya cli arg 8 months ago
JT 8cebc775e8
Merge pull request #515 from LykosAI/main 8 months ago
JT a58f4a9cc0
Merge pull request #566 from ionite34/downmerge 8 months ago
JT 393943abad
Merge branch 'dev' into downmerge 8 months ago
JT 79698b8b1f
Merge branch 'LykosAI:main' into main 8 months ago
JT d7086051d8 Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 8 months ago
JT f2051dfeb1
Merge pull request #565 from ionite34/diy-backport 8 months ago
JT e4aa0f85a5 oops forgot packaging 8 months ago
JT 4d42cab3f4 port kohya fixes to main 8 months ago
JT 94cb39ce2b
Merge pull request #564 from ionite34/fix-kohya 8 months ago
JT dada768a75 chagenlog 8 months ago
JT 67dd8c53b6 skip if no exist 8 months ago
JT e0f04bc87e handle both versions 8 months ago
JT 8fdd737c51 Fix paths for new kohya directory structure 8 months ago
JT 88504e0304
Merge pull request #562 from ionite34/downmerge 8 months ago
JT 99c8c926f2
Merge branch 'dev' into downmerge 8 months ago
Ionite b418a38ba5
Merge pull request #560 from ionite34/fix-kohya-dep 8 months ago
Ionite bd28f00d8b
Fix kohya_ss install 8 months ago
Ionite 807a20c25b
Merge pull request #555 from ionite34/inference-reference-controlnet 8 months ago
Ionite 8c2ea813db
Merge branch 'inference-reference-controlnet' of https://github.com/ionite34/StabilityMatrix into inference-reference-controlnet 8 months ago
Ionite ec46a9c275
Update CHANGELOG.md 8 months ago
Ionite 23a4ec9fd2
Merge branch 'dev' into inference-reference-controlnet 8 months ago
JT c1d697bfcd
Merge pull request #559 from ionite34/fix-teaching-tip 8 months ago
JT 0e6cf4db7e
chagenlog 8 months ago
Ionite 4be0ea4c8c
Merge branch 'dev' into inference-reference-controlnet 8 months ago
JT 17560f40fe
fix teaching tip spam 8 months ago
JT 02c64e2df1
Merge pull request #556 from ionite34/extra-confirm-delete 8 months ago
JT 7c2f51cc09 Really confirm package deletion 8 months ago
Ionite 1270c13ede
Use model merging instead 8 months ago
Ionite 80f9bf3a22
Add Strength based latent blend for ReferenceOnly 8 months ago
Ionite 2695f94b8b
Add LatentBlend 8 months ago
Ionite f378b8dac8
Fix image rescale size 8 months ago
Ionite f781fbb229
Fix ReferenceOnly ControlNet batch image outputting original as well 8 months ago
Ionite 1dfd2eb4e5
Fix Sampler to use temp model 8 months ago
Ionite d54e28acb4
Update LatentFromBatch to use TypedNodeBase 8 months ago
Ionite 810b22cd08
Fix extensions update requirement not updating 8 months ago
Ionite 3a3f94621b
Fix missing usings from merge 8 months ago
Ionite 0738c491af
Fix contract annotations for NullableExtensions 8 months ago
Ionite adaff0357e
Fix duplicate record 8 months ago
Ionite 425b343480
Merge remote-tracking branch 'origin/inference-reference-controlnet' into inference-reference-controlnet 8 months ago
Ionite 4bfa567ed4
Fix merge 8 months ago
Ionite 30609dc486
Fix merge 8 months ago
Ionite 664c8e5ba5
Fix merge 8 months ago
Ionite e7be967cdd
Reference controlnet refactors 8 months ago
Ionite 8b0bd6acbc
Merge branch 'dev' into inference-reference-controlnet 8 months ago
JT 86d33c952f
Merge pull request #553 from ionite34/downmerge 8 months ago
JT a6cb9751ac
Merge branch 'dev' into downmerge 8 months ago
Ionite cccedd3b9b
Merge pull request #551 from ionite34/inference-hires-sampler-addons 8 months ago
Ionite 030a25594c
Update CHANGELOG.md 8 months ago
Ionite eee89bba1a
Add HiresFix modules optionally inherit addon conditioning 8 months ago
JT c6ed1ba879
Merge pull request #549 from ionite34/faster-outputs 8 months ago
JT c885520193 Remove unused Uri thing 8 months ago
JT 8edafa642c Merge branch 'faster-outputs' of https://github.com/ionite34/StabilityMatrix into faster-outputs 8 months ago
JT 78ec821ff9 undo dotsettings 8 months ago
JT d8ad305391
Merge branch 'dev' into faster-outputs 8 months ago
JT e93a097f71 task cancellation working & editdiff on UI thread & dynamicdata for categories 8 months ago
Ionite 93b820c7e4
Merge pull request #550 from ionite34/changelog 8 months ago
Ionite 1fa934a5fa
Update CHANGELOG.md 8 months ago
Ionite b805a9e91c
Merge pull request #547 from ionite34/debug-image-memory 8 months ago
Ionite f9e305b803
Merge branch 'dev' into debug-image-memory 8 months ago
Ionite 6055b7454d
Merge pull request #541 from ionite34/inference-preprocessors 8 months ago
Ionite 75be254457
Merge branch 'dev' into inference-preprocessors 9 months ago
Ionite 9de2bfe668
Add Inference ControlNet Preprocessor preview button 9 months ago
JT 6fb5b0cb17 add the treeview back 9 months ago
JT c2b3337fe3 Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into faster-outputs 9 months ago
JT 9facb59287
Merge pull request #548 from ionite34/outputs-tree-view 9 months ago
JT 8bbeae6b5e moar chagenlog 9 months ago
JT aed6e79957 chagenlog 9 months ago
JT b394e5b701 faster outputs page with Task.Run & avalonia.labs async image 9 months ago
JT 987a224681 Merge branch 'outputs-tree-view' of https://github.com/ionite34/StabilityMatrix into outputs-tree-view 9 months ago
JT 09c72677b7 fix crash when directory not exist 9 months ago
Ionite d5f726d9d6
Add DebugExtractImagePromptsToTxt Command 9 months ago
JT 8c2e38e8ec
Merge branch 'dev' into outputs-tree-view 9 months ago
JT 7a4a403bdb
Merge pull request #534 from ionite34/multi-package 9 months ago
JT ea9dc5a3d2 Changed from a dropdown list to a TreeView for outputs browser 9 months ago
JT 2a574c2cd8 Merge branch 'multi-package' of https://github.com/ionite34/StabilityMatrix into multi-package 9 months ago
JT 9abbaa0f16 add teaching tip 9 months ago
Ionite a15dd34d97
Add Debug GC Collect and clear image cache commands 9 months ago
JT 29dd89550d
Merge branch 'dev' into multi-package 9 months ago
Ionite e94760b5ce
Improve ControlNet preprocessor serialization and non connected loading 9 months ago
Ionite c2e62926f3
Made RemoteName public to fix ControlNet preprocessor loading 9 months ago
JT 685662302d update localizations 9 months ago
JT 494f5f9e32 Merge branch 'multi-package' of https://github.com/ionite34/StabilityMatrix into multi-package 9 months ago
JT 8f53c281b3 updated one-click stuff & confirm exit dialog for multi-package 9 months ago
Ionite 842ab35e7d
Add Inference ControlNet preprocessor dimensions 9 months ago
JT 11ddd89b09
Merge branch 'dev' into multi-package 9 months ago
Ionite 96c30f5d40
Add out of date extension checks 9 months ago
Ionite d329f37e9d
Add version requirements for nodes 9 months ago
JT 24b34f76d6
Merge branch 'dev' into multi-package 9 months ago
JT 2303759eec redesigned packages page & made it the first tab & removed old launch page from tabs 9 months ago
Ionite ecc3b7da2d
Add extension manager update installed version info function 9 months ago
Ionite 3653fb475e
Fix missing node causing connection error 9 months ago
Ionite deb86fef83
Add ExtensionSpecifier parsing 9 months ago
Ionite cd0957825f
Merge branch 'dev' into inference-preprocessors 9 months ago
Ionite 4cccde8a4b
Implement preprocessor support for ControlNet module 9 months ago
Ionite a0221c7d91
Add Inference_Core_AIO_Preprocessor support 9 months ago
Ionite e1816f71c6
Add GetOptionalNodeOptionNames apis 9 months ago
Ionite dc4e99b489
Merge branch 'dev' into openart 9 months ago
JT 7e7b0148aa "fix" tests 9 months ago
JT 91fb39f366 fix some process exit stuff 9 months ago
JT 45f48ce36f fix inference launch connection stuff to work with multi-package 9 months ago
Ionite 1ea83f30f9
Add install node option and target package selection 9 months ago
Ionite e62546691a
Fix no download when no missing nodes 9 months ago
Ionite 0c30b4ba32
Fix openart extension install 9 months ago
Ionite df1f188fe6
Add GetInstalledExtensionsLiteAsync to interface 9 months ago
Ionite 92620dc894
Fix duplicate def 9 months ago
Ionite eafb905929
Merge branch 'dev' into openart 9 months ago
JT f7c99cdba9
Merge branch 'dev' into multi-package 9 months ago
JT eb78ec5010 added stop buttons 9 months ago
JT fb8f06d611 nav headers! and buttons! 9 months ago
JT d4354d0d20
Merge branch 'dev' into multi-package 9 months ago
JT 49273b049a wip start of multi-package running stuff 9 months ago
JT a1735e5837 remove windows references 9 months ago
JT fbc8111952 call feed endpoint if no search query & added sort for feed 9 months ago
JT b5529f6ece Added Installed Workflows tab 9 months ago
jt c7d6f66909 use extension manager instead 9 months ago
JT 7e3e520b75
Merge branch 'dev' into openart 9 months ago
JT be8d2cd78d now downloads & installs the required nodes & the workflow 9 months ago
JT 5e7b9286ac just put the whole json in there (for now) 9 months ago
JT 57a0597296 Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into openart 9 months ago
JT 889877724b
Merge branch 'dev' into openart 9 months ago
JT 9f430ecb65 settings n bug fixes 9 months ago
JT e87b5032be rearrange settings & add System.Drawing.Common reference? 9 months ago
JT c26960ce1f added basic card UI, searching, and infiniscroll 9 months ago
JT b4af5906fd Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into openart 9 months ago
JT 2998ceee21 Added models & refit interface for openart stuff 9 months ago
  1. 23
      .github/workflows/release.yml
  2. 2
      Avalonia.Gif/Avalonia.Gif.csproj
  3. 163
      CHANGELOG.md
  4. 5
      Directory.Build.props
  5. 56
      README.md
  6. 6
      StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
  7. 5
      StabilityMatrix.Avalonia/App.axaml
  8. 25
      StabilityMatrix.Avalonia/App.axaml.cs
  9. 14
      StabilityMatrix.Avalonia/Assets/sitecustomize.py
  10. 208
      StabilityMatrix.Avalonia/Controls/BetterContextDragBehavior.cs
  11. 80
      StabilityMatrix.Avalonia/Controls/Inference/ControlNetCard.axaml
  12. 12
      StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml
  13. 9
      StabilityMatrix.Avalonia/Controls/Inference/ImageGalleryCard.axaml
  14. 48
      StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml
  15. 7
      StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml.cs
  16. 30
      StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml
  17. 2
      StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml.cs
  18. 6
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml
  19. 8
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs
  20. 20
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/AsyncImageFailedEventArgs.cs
  21. 42
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Events.cs
  22. 135
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Properties.cs
  23. 248
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.cs
  24. 565
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheBase.cs
  25. 18
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheOptions.cs
  26. 36
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/FileCache.cs
  27. 76
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/ImageCache.cs
  28. 156
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorage.cs
  29. 49
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorageItem.cs
  30. 21
      StabilityMatrix.Avalonia/Controls/VendorLabs/LICENSE
  31. 48
      StabilityMatrix.Avalonia/Controls/VendorLabs/Themes/BetterAsyncImage.axaml
  32. 1
      StabilityMatrix.Avalonia/Converters/FileUriConverter.cs
  33. 119
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  34. 3
      StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs
  35. 93
      StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs
  36. 14
      StabilityMatrix.Avalonia/Extensions/DataObjectExtensions.cs
  37. 9
      StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs
  38. 2
      StabilityMatrix.Avalonia/Helpers/UriHandler.cs
  39. 153
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  40. 116
      StabilityMatrix.Avalonia/Languages/Resources.es.resx
  41. 50
      StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx
  42. 51
      StabilityMatrix.Avalonia/Languages/Resources.resx
  43. 114
      StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx
  44. 8
      StabilityMatrix.Avalonia/Models/IInfinitelyScroll.cs
  45. 3
      StabilityMatrix.Avalonia/Models/ImageSource.cs
  46. 51
      StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs
  47. 10
      StabilityMatrix.Avalonia/Models/OpenArtCustomNode.cs
  48. 22
      StabilityMatrix.Avalonia/Models/OpenArtMetadata.cs
  49. 5
      StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs
  50. 21
      StabilityMatrix.Avalonia/Models/PackageSteps/UnpackSiteCustomizeStep.cs
  51. 1
      StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs
  52. 8
      StabilityMatrix.Avalonia/Services/IModelDownloadLinkHandler.cs
  53. 3
      StabilityMatrix.Avalonia/Services/INavigationService.cs
  54. 20
      StabilityMatrix.Avalonia/Services/InferenceClientManager.cs
  55. 251
      StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs
  56. 5
      StabilityMatrix.Avalonia/Services/NavigationService.cs
  57. 153
      StabilityMatrix.Avalonia/Services/RunningPackageService.cs
  58. 14
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  59. 3
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings
  60. 73
      StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml
  61. 403
      StabilityMatrix.Avalonia/Styles/CommandBarButtonStyles.axaml
  62. 41
      StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml
  63. 1
      StabilityMatrix.Avalonia/Styles/ThemeColors.axaml
  64. 119
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
  65. 21
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs
  66. 3
      StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs
  67. 34
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
  68. 220
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs
  69. 3
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
  70. 20
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
  71. 24
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
  72. 3
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  73. 15
      StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs
  74. 11
      StabilityMatrix.Avalonia/ViewModels/Dialogs/LykosLoginViewModel.cs
  75. 26
      StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs
  76. 7
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
  77. 185
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs
  78. 5
      StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackagesItemViewModel.cs
  79. 4
      StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelItemViewModel.cs
  80. 13
      StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs
  81. 25
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
  82. 9
      StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs
  83. 84
      StabilityMatrix.Avalonia/ViewModels/Inference/ControlNetCardViewModel.cs
  84. 36
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToImageViewModel.cs
  85. 5
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
  86. 15
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs
  87. 19
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
  88. 100
      StabilityMatrix.Avalonia/ViewModels/Inference/LayerDiffuseCardViewModel.cs
  89. 119
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs
  90. 57
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/HiresFixModule.cs
  91. 26
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/LayerDiffuseModule.cs
  92. 30
      StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs
  93. 75
      StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs
  94. 144
      StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs
  95. 173
      StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs
  96. 33
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  97. 3
      StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs
  98. 7
      StabilityMatrix.Avalonia/ViewModels/NewPackageManagerViewModel.cs
  99. 347
      StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs
  100. 188
      StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
  101. Some files were not shown because too many files have changed in this diff Show More

23
.github/workflows/release.yml

@ -76,20 +76,15 @@ jobs:
echo "Using version ${{ github.event.inputs.version }}"
echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
- name: Set up .NET 6 (for PupNet)
- name: Set up .NET 8
uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'
dotnet-version: '8.0.x'
- name: Install PupNet
run: |
sudo apt-get -y install libfuse2
dotnet tool install --framework net6.0 -g KuiperZone.PupNet
- name: Set up .NET 8
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
dotnet tool install -g KuiperZone.PupNet
- name: PupNet Build
env:
@ -182,7 +177,7 @@ jobs:
platform-id: osx-arm64
app-name: "Stability Matrix.app"
out-name: "StabilityMatrix-macos-arm64.dmg"
runs-on: macos-13
runs-on: macos-14
steps:
- uses: actions/checkout@v3
@ -262,13 +257,11 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: '20.x'
node-version: '20.11.x'
- name: Install dependencies for dmg creation
run: >
npm install --global create-dmg
brew install graphicsmagick imagemagick
run: brew install graphicsmagick imagemagick && npm install --global create-dmg
- name: Create dmg
working-directory: signing
run: >

2
Avalonia.Gif/Avalonia.Gif.csproj

@ -10,7 +10,7 @@
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.9" />
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="SkiaSharp" Version="2.88.7" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
</ItemGroup>

163
CHANGELOG.md

@ -5,6 +5,167 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
## v2.10.3
### Changed
- Centered OpenArt browser cards
### Fixed
- Fixed MPS install on macOS for ComfyUI, A1111, SDWebUI Forge, and SDWebUI UX causing torch to be upgraded to dev nightly versions and causing incompatibilities with dependencies.
- Fixed crash when failing to parse Python package details
- Fixed "Auto Scroll to End" not working in some scenarios
- Fixed "Auto Scroll to End" toggle button not scrolling to the end when toggled on
- Fixed/reverted output folder name changes for Automatic1111
- Fixed xformers being uninstalled with every ComfyUI update
- Fixed missing progress text during package installs
## v2.10.2
### Changed
- Updated translations for Spanish and Turkish
### Fixed
- Fixed more crashes when loading invalid connected model info files
- Fixed pip installs not parsing comments properly
- Fixed crash when sending input to a process that isn't running
- Fixed breadcrumb on console page showing incorrect running package name
- Fixed [#576](https://github.com/LykosAI/StabilityMatrix/issues/576) - drag & drop crashes on macOS & Linux
- Fixed [#594](https://github.com/LykosAI/StabilityMatrix/issues/594) - missing thumbnails in Inference model selector
- Fixed [#600](https://github.com/LykosAI/StabilityMatrix/issues/600) - kohya_ss v24+ not launching
- Downgraded Avalonia back to 11.0.9 to fix [#589](https://github.com/LykosAI/StabilityMatrix/issues/589) and possibly other rendering issues
## v2.10.1
### Added
- Added SVD Shared Model & Output Folders for Forge (fixes [#580](https://github.com/LykosAI/StabilityMatrix/issues/580))
### Changed
- Improved error message when logging in with a Lykos account fails due to incorrect email or password
- Model Browser & Workflow Browser now auto-load when first navigating to those pages
- Removed update confirmation dialog, instead showing the new version in the update button tooltip
### Fixed
- Fixed package launch not working when environment variable `SETUPTOOLS_USE_DISTUTILS` is set due to conflict with a default environment variable. User environment variables will now correctly override any default environment variables.
- Fixed "No refresh token found" error when failing to login with Lykos account in some cases
- Fixed blank entries appearing in the Categories dropdown on the Checkpoints page
- Fixed crash when loading invalid connected model info files
- Fixed [#585](https://github.com/LykosAI/StabilityMatrix/issues/585) - Crash when drag & drop source and destination are the same
- Fixed [#584](https://github.com/LykosAI/StabilityMatrix/issues/584) - `--launch-package` argument not working
- Fixed [#581](https://github.com/LykosAI/StabilityMatrix/issues/581) - Inference teaching tip showing more often than it should
- Fixed [#578](https://github.com/LykosAI/StabilityMatrix/issues/578) - "python setup.py egg_info did not run successfully" failure when installing Auto1111 or SDWebUI Forge
- Fixed [#574](https://github.com/LykosAI/StabilityMatrix/issues/574) - local images not showing on macOS or Linux
## v2.10.0
### Added
- Added Reference-Only mode for Inference ControlNet, used for guiding the sampler with an image without a pretrained model. Part of the latent and attention layers will be connected to the reference image, similar to Image to Image or Inpainting.
- Inference ControlNet module now supports over 42 preprocessors, a new button next to the preprocessors dropdown allows previewing the output of the selected preprocessor on the image.
- Added resolution selection for Inference ControlNet module, this controls preprocessor resolution too.
- Added Layer Diffuse sampler addon to Inference, allows generating foreground with transparency with SD1.5 and SDXL.
- Added support for deep links from the new Stability Matrix Chrome extension
- Added OpenArt.AI workflow browser for ComfyUI workflows
- Added more metadata to the image dialog info flyout
- Added Output Sharing toggle in Advanced Options during install flow
### Changed
- Revamped the Packages page to enable running multiple packages at the same time
- Changed the Outputs Page to use a TreeView for the directory selection instead of a dropdown selector
- Model download location selector now searches all subfolders
- Inference Primary Sampler Addons (i.e. ControlNet, FreeU) are now inherited by Hires Fix Samplers, this can be overriden from the Hires Fix module's settings menu by disabling the "Inherit Primary Sampler Addons" option.
- Revisited the way images are loaded on the outputs page, with improvements to loading speed & not freezing the UI while loading
- Updated translations for French, Spanish, and Turkish
- Changed to a new image control for pages with many images
- (Internal) Updated to Avalonia 11.0.10
### Fixed
- Fixed [#559](https://github.com/LykosAI/StabilityMatrix/issues/559) - "Unable to load bitmap from provided data" error in Checkpoints page
- Fixed [#522](https://github.com/LykosAI/StabilityMatrix/issues/522) - Incorrect output directory path for latest Auto1111
- Fixed [#529](https://github.com/LykosAI/StabilityMatrix/issues/529) - OneTrainer requesting input during update
- Fixed Civitai model browser error when sorting by Installed with more than 100 installed models
- Fixed CLIP Install errors due to setuptools distutils conflict, added default environment variable setting `SETUPTOOLS_USE_DISTUTILS=stdlib`
- Fixed progress bars not displaying properly during package installs & updates
- Fixed ComfyUI extension updates not running install.py / updating requirements.txt
- Improved performance when deleting many images from the Outputs page
- Fixed ComfyUI torch downgrading to 2.1.2 when updating
- Fixed Inference HiresFix module "Inherit Primary Sampler Addons" setting not effectively disabling when unchecked
- Fixed model download location options for VAEs in the CivitAI Model Browser
### Removed
- Removed the main Launch page, as it is no longer needed with the new Packages page
## v2.10.0-pre.2
### Added
- Added more metadata to the image dialog info flyout
- Added Restart button to console page
### Changed
- Model download location selector now searches all subfolders
### Fixed
- Fixed Civitai model browser not showing images when "Show NSFW" is disabled
- Fixed crash when Installed Workflows page is opened with no Workflows folder
- Fixed progress bars not displaying properly during package installs & updates
- Fixed ComfyUI extension updates not running install.py / updating requirements.txt
## v2.10.0-pre.1
### Added
- Added OpenArt.AI workflow browser for ComfyUI workflows
- Added Output Sharing toggle in Advanced Options during install flow
### Changed
- Changed to a new image control for pages with many images
- Removed Symlink option for InvokeAI due to changes with InvokeAI v4.0+
- Output sharing is now enabled by default for new installations
- (Internal) Updated to Avalonia 11.0.10
### Fixed
- Improved performance when deleting many images from the Outputs page
- Fixed ComfyUI torch downgrading to 2.1.2 when updating
- Fixed [#529](https://github.com/LykosAI/StabilityMatrix/issues/529) - OneTrainer requesting input during update
- Fixed "Could not find entry point for InvokeAI" error on InvokeAI v4.0+
## v2.10.0-dev.3
### Added
- Added support for deep links from the new Stability Matrix Chrome extension
### Changed
- Due to changes on the CivitAI API, you can no longer select a specific page in the CivitAI Model Browser
- Due to the above API changes, new pages are now loaded via "infinite scrolling"
### Fixed
- Fixed Inference HiresFix module "Inherit Primary Sampler Addons" setting not effectively disabling when unchecked
- Fixed model download location options for VAEs in the CivitAI Model Browser
- Fixed crash on startup when library directory is not set
- Fixed One-Click install progress dialog not disappearing after completion
- Fixed ComfyUI with Inference pop-up during one-click install appearing below the visible scroll area
- Fixed no packages being available for one-click install on PCs without a GPU
- Fixed models not being removed from the installed models cache when deleting them from the Checkpoints page
- Fixed missing ratings on some models in the CivitAI Model Browser
- Fixed missing favorite count in the CivitAI Model Browser
- Fixed recommended models not showing all SDXL models
## v2.10.0-dev.2
### Added
- Added Reference-Only mode for Inference ControlNet, used for guiding the sampler with an image without a pretrained model. Part of the latent and attention layers will be connected to the reference image, similar to Image to Image or Inpainting.
### Changed
- Inference Primary Sampler Addons (i.e. ControlNet, FreeU) are now inherited by Hires Fix Samplers, this can be overriden from the Hires Fix module's settings menu by disabling the "Inherit Primary Sampler Addons" option.
- Revisited the way images are loaded on the outputs page, with improvements to loading speed & not freezing the UI while loading
### Fixed
- Fixed Outputs page not remembering where the user last was in the TreeView in certain circumstances
- Fixed Inference extension upgrades not being added to missing extensions list for prompted install
- Fixed "The Open Web UI button has moved" teaching tip spam
## v2.10.0-dev.1
### Added
- Inference ControlNet module now supports over 42 preprocessors, a new button next to the preprocessors dropdown allows previewing the output of the selected preprocessor on the image.
- Added resolution selection for Inference ControlNet module, this controls preprocessor resolution too.
### Changed
- Revamped the Packages page to enable running multiple packages at the same time
- Changed the Outputs Page to use a TreeView for the directory selection instead of a dropdown selector
### Removed
- Removed the main Launch page, as it is no longer needed with the new Packages page
## v2.9.3
### Changed
- Removed Symlink option for InvokeAI to prevent InvokeAI from moving models into its own directories (will be replaced with a Config option in a future update)
### Fixed
- Fixed images not appearing in Civitai Model Browser when "Show NSFW" was disabled
- Fixed [#556](https://github.com/LykosAI/StabilityMatrix/issues/556) - "Could not find entry point for InvokeAI" error
## v2.9.2
### Changed
- Due to changes with the CivitAI API, you can no longer select a specific page in the CivitAI Model Browser
- Due to the above API changes, new pages are now loaded via "infinite scrolling"
### Fixed
- Fixed models not being removed from the installed models cache when deleting them from the Checkpoints page
- Fixed model download location options for VAEs in the CivitAI Model Browser
- Fixed One-Click install progress dialog not disappearing after completion
- Fixed ComfyUI with Inference pop-up during one-click install appearing below the visible scroll area
- Fixed no packages being available for one-click install on PCs without a GPU
## v2.9.1
### Added
- Fixed [#498](https://github.com/LykosAI/StabilityMatrix/issues/498) Added "Pony" category to CivitAI Model Browser
@ -14,6 +175,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
- Fixed [#502](https://github.com/LykosAI/StabilityMatrix/issues/502) - missing launch options for Forge
- Fixed [#500](https://github.com/LykosAI/StabilityMatrix/issues/500) - missing output images in Forge when using output sharing
- Fixed [#490](https://github.com/LykosAI/StabilityMatrix/issues/490) - `mpmath has no attribute 'rational'` error on macOS
- Fixed [#510](https://github.com/ionite34/StabilityMatrix/pull/564/files) - kohya_ss packages with v23.0.x failing to install due to missing 'packaging' dependency
- Fixed incorrect progress text when deleting a checkpoint from the Checkpoints page
- Fixed incorrect icon colors on macOS
@ -60,6 +222,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
- Added copy image support on linux and macOS for Inference outputs viewer menu
### Fixed
- Fixed StableSwarmUI not installing properly on macOS
- Fixed output sharing for Stable Diffusion WebUI Forge
- Hopefully actually fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS
- Fixed default command line args for SDWebUI Forge on macOS
- Fixed output paths and output sharing for SDWebUI Forge

5
Directory.Build.props

@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<AvaloniaVersion>11.0.9</AvaloniaVersion>
</PropertyGroup>
</Project>

56
README.md

@ -2,14 +2,16 @@
[![Build](https://github.com/LykosAI/StabilityMatrix/actions/workflows/build.yml/badge.svg)](https://github.com/LykosAI/StabilityMatrix/actions/workflows/build.yml)
[![Discord Server](https://img.shields.io/discord/1115555685476868168?logo=discord&logoColor=white&label=Discord%20Server)](https://discord.com/invite/TUrgfECxHz)
[![Release](https://img.shields.io/github/v/release/LykosAI/StabilityMatrix?label=Latest%20Release&link=https%3A%2F%2Fgithub.com%2FLykosAI%2FStabilityMatrix%2Freleases%2Flatest)][release]
[release]: https://github.com/LykosAI/StabilityMatrix/releases/latest
[download-win-x64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-win-x64.zip
[download-linux-x64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-linux-x64.zip
[download-linux-appimage-x64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-linux-x64.zip
[download-linux-aur-x64]: https://aur.archlinux.org/packages/stabilitymatrix
[download-macos-arm64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-macos-arm64.dmg
[auto1111]: https://github.com/AUTOMATIC1111/stable-diffusion-webui
[auto1111-directml]: https://github.com/lshqqytiger/stable-diffusion-webui-directml
[sdwebui-directml]: https://github.com/lshqqytiger/stable-diffusion-webui-directml
[webui-ux]: https://github.com/anapnoe/stable-diffusion-webui-ux
[comfy]: https://github.com/comfyanonymous/ComfyUI
[sdnext]: https://github.com/vladmandic/automatic
@ -21,48 +23,37 @@
[fooocus-controlnet]: https://github.com/fenneishi/Fooocus-ControlNet-SDXL
[kohya-ss]: https://github.com/bmaltais/kohya_ss
[onetrainer]: https://github.com/Nerogar/OneTrainer
[forge]: https://github.com/lllyasviel/stable-diffusion-webui-forge
[stable-swarm]: https://github.com/Stability-AI/StableSwarmUI
[civitai]: https://civitai.com/
[huggingface]: https://huggingface.co/
Multi-Platform Package Manager and Inference UI for Stable Diffusion
![Header image for Stability Matrix, Multi-Platform Package Manager and Inference UI for Stable Diffusion](https://cdn.lykos.ai/static/sm-banner-rounded.webp)
### ✨ New in 2.5 - [Inference](#inference-A-reimagined-built-in-Stable-Diffusion-experience), a built-in interface for Stable Diffusion powered by ComfyUI
[![Windows](https://img.shields.io/badge/Windows%2010,%2011-%230079d5.svg?style=for-the-badge&logo=Windows%2011&logoColor=white)][download-win-x64]
[![Linux (AppImage)](https://img.shields.io/badge/Linux%20(AppImage)-FCC624?style=for-the-badge&logo=linux&logoColor=black)][download-linux-appimage-x64]
[![Arch Linux (AUR)](https://img.shields.io/badge/Arch%20Linux%20(AUR)-1793D1?style=for-the-badge&logo=archlinux&logoColor=white)][download-linux-aur-x64]
[![macOS](https://img.shields.io/badge/mac%20os%20%28apple%20silicon%29-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)][download-macos-arm64]
Multi-Platform Package Manager and Inference UI for Stable Diffusion
### 🖱 One click install and update for Stable Diffusion Web UI Packages
- Supports:
- [Automatic 1111][auto1111], [Automatic 1111 DirectML][auto1111-directml], [SD Web UI-UX][webui-ux], [SD.Next][sdnext]
- [Stable Diffusion WebUI Forge][forge], [Automatic 1111][auto1111], [Automatic 1111 DirectML][auto1111-directml], [SD Web UI-UX][webui-ux], [SD.Next][sdnext]
- [Fooocus][fooocus], [Fooocus MRE][fooocus-mre], [Fooocus ControlNet SDXL][fooocus-controlnet], [Ruined Fooocus][ruined-fooocus]
- [ComfyUI][comfy]
- [StableSwarmUI][stable-swarm]
- [VoltaML][voltaml]
- [InvokeAI][invokeai]
- [Kohya's GUI][kohya-ss]
- [OneTrainer][onetrainer]
- Manage plugins / extensions for supported packages ([Automatic 1111][auto1111], [Comfy UI][comfy])
- Manage plugins / extensions for supported packages ([Automatic1111][auto1111], [Comfy UI][comfy], [SD Web UI-UX][webui-ux], and [SD.Next][sdnext])
- Easily install or update Python dependencies for each package
- Embedded Git and Python dependencies, with no need for either to be globally installed
- Fully portable - move Stability Matrix's Data Directory to a new drive or computer at any time
### 🚀 Launcher with syntax highlighted terminal emulator, routed GUI input prompts
- Launch arguments editor with predefined or custom options for each Package install
- Configurable Environment Variables
### 🗃 Checkpoint Manager, configured to be shared by all Package installs
- Option to find CivitAI metadata and preview thumbnails for new local imports
### ☁ Model Browser to import from [CivitAI][civitai] and [HuggingFace][huggingface]
- Automatically imports to the associated model folder depending on the model type
- Downloads relevant metadata files and preview image
![header](https://cdn.lykos.ai/static/sm-banner-rounded.webp)
[![Release](https://img.shields.io/github/v/release/LykosAI/StabilityMatrix?label=Latest%20Release&link=https%3A%2F%2Fgithub.com%2FLykosAI%2FStabilityMatrix%2Freleases%2Flatest)][release]
[![Windows](https://img.shields.io/badge/Windows%2010,%2011-%230079d5.svg?style=for-the-badge&logo=Windows%2011&logoColor=white)][download-win-x64]
[![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black)][download-linux-x64]
[![macOS](https://img.shields.io/badge/mac%20os%20%28apple%20silicon%29-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)][download-macos-arm64]
### Inference - A reimagined built-in Stable Diffusion experience
### ✨ Inference - A Reimagined Interface for Stable Diffusion, Built-In to Stability Matrix
- Powerful auto-completion and syntax highlighting using a formal language grammar
- Workspaces open in tabs that save and load from `.smproj` project files
@ -76,13 +67,20 @@ Multi-Platform Package Manager and Inference UI for Stable Diffusion
<img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/4341cc34-a584-4e9c-bb3b-276009bdae80" alt=""/>
</p>
### Searchable launch options
### 🚀 Launcher with syntax highlighted terminal emulator, routed GUI input prompts
- Launch arguments editor with predefined or custom options for each Package install
- Configurable Environment Variables
<p align="center">
<img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/75456866-9d95-47c6-8c0a-fdc19443ee02" alt=""/>
</p>
### Model browser powered by [Civit AI][civitai]
- Downloads new models, automatically uses the appropriate shared model directory
### 🗃 Checkpoint Manager, configured to be shared by all Package installs
- Option to find CivitAI metadata and preview thumbnails for new local imports
### ☁ Model Browser to import from [CivitAI][civitai] and [HuggingFace][huggingface]
- Automatically imports to the associated model folder depending on the model type
- Downloads relevant metadata files and preview image
- Pause and resume downloads, even after closing the app
<p align="center">

6
StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj

@ -19,12 +19,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.9" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.9" />
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="$(AvaloniaVersion)" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="NLog" Version="5.2.8" />
</ItemGroup>

5
StabilityMatrix.Avalonia/App.axaml

@ -4,6 +4,7 @@
xmlns:local="using:StabilityMatrix.Avalonia"
xmlns:idcr="using:Dock.Avalonia.Controls.Recycling"
xmlns:styling="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
Name="Stability Matrix"
RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
@ -28,6 +29,7 @@
<ResourceInclude Source="Styles/ListBoxStyles.axaml"/>
<ResourceInclude Source="Styles/FAComboBoxStyles.axaml"/>
<ResourceInclude Source="Controls/Inference/ImageFolderCard.axaml"/>
<ResourceInclude Source="Controls/VendorLabs/Themes/BetterAsyncImage.axaml"/>
</ResourceDictionary.MergedDictionaries>
<idcr:ControlRecycling x:Key="ControlRecyclingKey" />
@ -54,6 +56,7 @@
<StyleInclude Source="Styles/DockStyles.axaml"/>
<StyleInclude Source="Styles/BorderStyles.axaml"/>
<StyleInclude Source="Styles/TextBoxStyles.axaml"/>
<StyleInclude Source="Styles/CommandBarButtonStyles.axaml"/>
<StyleInclude Source="Controls/AdvancedImageBox.axaml"/>
<StyleInclude Source="Controls/FrameCarousel.axaml"/>
<StyleInclude Source="Controls/CodeCompletion/CompletionWindow.axaml"/>
@ -79,6 +82,8 @@
<StyleInclude Source="Controls/Inference/FreeUCard.axaml"/>
<StyleInclude Source="Controls/Inference/ControlNetCard.axaml"/>
<StyleInclude Source="Controls/Inference/PromptExpansionCard.axaml"/>
<StyleInclude Source="Controls/Inference/LayerDiffuseCard.axaml"/>
<labs:ControlThemes/>
<Style Selector="DockControl">
<Setter Property="(DockProperties.ControlRecycling)" Value="{StaticResource ControlRecyclingKey}" />

25
StabilityMatrix.Avalonia/App.axaml.cs

@ -40,7 +40,6 @@ using Polly.Extensions.Http;
using Polly.Timeout;
using Refit;
using Sentry;
using StabilityMatrix.Avalonia.DesignData;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
@ -54,14 +53,12 @@ using StabilityMatrix.Core.Converters.Json;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Configs;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services;
using Application = Avalonia.Application;
using DrawingColor = System.Drawing.Color;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
#if DEBUG
using StabilityMatrix.Avalonia.Diagnostics.LogViewer;
@ -323,17 +320,18 @@ public sealed class App : Application
provider.GetRequiredService<IDiscordRichPresenceService>(),
provider.GetRequiredService<ServiceManager<ViewModelBase>>(),
provider.GetRequiredService<ITrackedDownloadService>(),
provider.GetRequiredService<IModelIndexService>()
provider.GetRequiredService<IModelIndexService>(),
provider.GetRequiredService<Lazy<IModelDownloadLinkHandler>>()
)
{
Pages =
{
provider.GetRequiredService<LaunchPageViewModel>(),
provider.GetRequiredService<InferenceViewModel>(),
provider.GetRequiredService<NewPackageManagerViewModel>(),
provider.GetRequiredService<InferenceViewModel>(),
provider.GetRequiredService<CheckpointsPageViewModel>(),
provider.GetRequiredService<CheckpointBrowserViewModel>(),
provider.GetRequiredService<OutputsPageViewModel>()
provider.GetRequiredService<OutputsPageViewModel>(),
provider.GetRequiredService<WorkflowsPageViewModel>()
},
FooterPages = { provider.GetRequiredService<SettingsViewModel>() }
}
@ -556,7 +554,7 @@ public sealed class App : Application
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://civitai.com");
c.Timeout = TimeSpan.FromSeconds(15);
c.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(retryPolicy);
@ -565,7 +563,7 @@ public sealed class App : Application
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://civitai.com");
c.Timeout = TimeSpan.FromSeconds(15);
c.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(retryPolicy);
@ -583,6 +581,15 @@ public sealed class App : Application
new TokenAuthHeaderHandler(serviceProvider.GetRequiredService<LykosAuthTokenProvider>())
);
services
.AddRefitClient<IOpenArtApi>(defaultRefitSettings)
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://openart.ai/api/public/workflows");
c.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(retryPolicy);
// Add Refit client managers
services.AddHttpClient("A3Client").AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy));

14
StabilityMatrix.Avalonia/Assets/sitecustomize.py

@ -68,6 +68,20 @@ def _patch_rich_console():
pass
except Exception as e:
print("[sitecustomize error]:", e)
try:
from pip._vendor.rich import console
class _Console(console.Console):
@property
def is_terminal(self) -> bool:
return True
console.Console = _Console
except ImportError:
pass
except Exception as e:
print("[sitecustomize error]:", e)
_patch_rich_console()

208
StabilityMatrix.Avalonia/Controls/BetterContextDragBehavior.cs

@ -0,0 +1,208 @@
using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Xaml.Interactions.DragAndDrop;
using Avalonia.Xaml.Interactivity;
namespace StabilityMatrix.Avalonia.Controls;
public class BetterContextDragBehavior : Behavior<Control>
{
private Point _dragStartPoint;
private PointerEventArgs? _triggerEvent;
private bool _lock;
private bool _captured;
public static readonly StyledProperty<object?> ContextProperty = AvaloniaProperty.Register<
ContextDragBehavior,
object?
>(nameof(Context));
public static readonly StyledProperty<IDragHandler?> HandlerProperty = AvaloniaProperty.Register<
ContextDragBehavior,
IDragHandler?
>(nameof(Handler));
public static readonly StyledProperty<double> HorizontalDragThresholdProperty = AvaloniaProperty.Register<
ContextDragBehavior,
double
>(nameof(HorizontalDragThreshold), 3);
public static readonly StyledProperty<double> VerticalDragThresholdProperty = AvaloniaProperty.Register<
ContextDragBehavior,
double
>(nameof(VerticalDragThreshold), 3);
public static readonly StyledProperty<string> DataFormatProperty = AvaloniaProperty.Register<
BetterContextDragBehavior,
string
>("DataFormat");
public string DataFormat
{
get => GetValue(DataFormatProperty);
set => SetValue(DataFormatProperty, value);
}
public object? Context
{
get => GetValue(ContextProperty);
set => SetValue(ContextProperty, value);
}
public IDragHandler? Handler
{
get => GetValue(HandlerProperty);
set => SetValue(HandlerProperty, value);
}
public double HorizontalDragThreshold
{
get => GetValue(HorizontalDragThresholdProperty);
set => SetValue(HorizontalDragThresholdProperty, value);
}
public double VerticalDragThreshold
{
get => GetValue(VerticalDragThresholdProperty);
set => SetValue(VerticalDragThresholdProperty, value);
}
/// <inheritdoc />
protected override void OnAttachedToVisualTree()
{
AssociatedObject?.AddHandler(
InputElement.PointerPressedEvent,
AssociatedObject_PointerPressed,
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble
);
AssociatedObject?.AddHandler(
InputElement.PointerReleasedEvent,
AssociatedObject_PointerReleased,
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble
);
AssociatedObject?.AddHandler(
InputElement.PointerMovedEvent,
AssociatedObject_PointerMoved,
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble
);
AssociatedObject?.AddHandler(
InputElement.PointerCaptureLostEvent,
AssociatedObject_CaptureLost,
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble
);
}
/// <inheritdoc />
protected override void OnDetachedFromVisualTree()
{
AssociatedObject?.RemoveHandler(InputElement.PointerPressedEvent, AssociatedObject_PointerPressed);
AssociatedObject?.RemoveHandler(InputElement.PointerReleasedEvent, AssociatedObject_PointerReleased);
AssociatedObject?.RemoveHandler(InputElement.PointerMovedEvent, AssociatedObject_PointerMoved);
AssociatedObject?.RemoveHandler(InputElement.PointerCaptureLostEvent, AssociatedObject_CaptureLost);
}
private async Task DoDragDrop(PointerEventArgs triggerEvent, object? value)
{
var data = new DataObject();
data.Set(DataFormat, value!);
var effect = DragDropEffects.None;
if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Alt))
{
effect |= DragDropEffects.Link;
}
else if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Shift))
{
effect |= DragDropEffects.Move;
}
else if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Control))
{
effect |= DragDropEffects.Copy;
}
else
{
effect |= DragDropEffects.Move;
}
await DragDrop.DoDragDrop(triggerEvent, data, effect);
}
private void Released()
{
_triggerEvent = null;
_lock = false;
}
private void AssociatedObject_PointerPressed(object? sender, PointerPressedEventArgs e)
{
var properties = e.GetCurrentPoint(AssociatedObject).Properties;
if (properties.IsLeftButtonPressed)
{
if (e.Source is Control control && AssociatedObject?.DataContext == control.DataContext)
{
_dragStartPoint = e.GetPosition(null);
_triggerEvent = e;
_lock = true;
_captured = true;
}
}
}
private void AssociatedObject_PointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (_captured)
{
if (e.InitialPressMouseButton == MouseButton.Left && _triggerEvent is { })
{
Released();
}
_captured = false;
}
}
private async void AssociatedObject_PointerMoved(object? sender, PointerEventArgs e)
{
var properties = e.GetCurrentPoint(AssociatedObject).Properties;
if (_captured && properties.IsLeftButtonPressed && _triggerEvent is { })
{
var point = e.GetPosition(null);
var diff = _dragStartPoint - point;
var horizontalDragThreshold = HorizontalDragThreshold;
var verticalDragThreshold = VerticalDragThreshold;
if (Math.Abs(diff.X) > horizontalDragThreshold || Math.Abs(diff.Y) > verticalDragThreshold)
{
if (_lock)
{
_lock = false;
}
else
{
return;
}
var context = Context ?? AssociatedObject?.DataContext;
Handler?.BeforeDragDrop(sender, _triggerEvent, context);
await DoDragDrop(_triggerEvent, context);
Handler?.AfterDragDrop(sender, _triggerEvent, context);
_triggerEvent = null;
}
}
}
private void AssociatedObject_CaptureLost(object? sender, PointerCaptureLostEventArgs e)
{
Released();
_captured = false;
}
}

80
StabilityMatrix.Avalonia/Controls/Inference/ControlNetCard.axaml

@ -36,29 +36,46 @@
<sg:SpacedGrid
ColumnDefinitions="0.34*,0.65*"
ColumnSpacing="8"
RowDefinitions="Auto,Auto,Auto"
RowDefinitions="Auto,Auto,Auto,Auto"
RowSpacing="4">
<!-- Image Select -->
<controls:SelectImageCard
Grid.RowSpan="2"
Grid.RowSpan="3"
Padding="6"
VerticalAlignment="Stretch"
DataContext="{Binding SelectImageCardViewModel}"
FontSize="13" />
<!-- TODO: Preprocessor Model -->
<ui:FAComboBox
<!-- Preprocessor Model -->
<sg:SpacedGrid
Grid.Row="0"
Grid.Column="1"
Margin="0,0,0,4"
HorizontalAlignment="Stretch"
markupExtensions:ShowDisabledTooltipExtension.ShowOnDisabled="True"
Header="{x:Static lang:Resources.Label_Preprocessor}"
IsEnabled="False"
Theme="{StaticResource FAComboBoxHybridModelTheme}"
ToolTip.ShowDelay="1200"
ToolTip.Tip="{StaticResource WipFeatureToolTip}" />
ColumnDefinitions="*,Auto">
<ui:FAComboBox
Margin="0,0,0,4"
SelectedItem="{Binding SelectedPreprocessor}"
ItemsSource="{Binding ClientManager.Preprocessors}"
DisplayMemberBinding="{Binding DisplayName}"
HorizontalAlignment="Stretch"
Header="{x:Static lang:Resources.Label_Preprocessor}"/>
<Button
ToolTip.Tip="{x:Static lang:Resources.Action_PreviewPreprocessor}"
Padding="7"
VerticalAlignment="Center"
Margin="0,23,0,0"
Command="{Binding PreviewPreprocessorCommand}"
CommandParameter="{Binding SelectedPreprocessor}"
Grid.Column="1">
<fluentIcons:SymbolIcon
VerticalAlignment="Center"
FontSize="15"
IsFilled="True"
Symbol="Play" />
</Button>
</sg:SpacedGrid>
<!-- ControlNet Model -->
<ui:FAComboBox
@ -101,6 +118,45 @@
</ui:FAComboBox.DataTemplates>
</ui:FAComboBox>
<!-- Dimensions -->
<Grid
Grid.Row="2"
Grid.Column="1"
Margin="0,4,0,0"
ColumnDefinitions="*,Auto,*"
RowDefinitions="Auto,*">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="0,0,4,4"
Text="{x:Static lang:Resources.Label_Width}"/>
<ui:NumberBox
Grid.Row="1"
Grid.Column="0"
Margin="0,0,4,0"
PlaceholderText="128"
SmallChange="128"
Value="{Binding Width}"
ValidationMode="InvalidInputOverwritten"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Compact"/>
<TextBlock
Grid.Row="0"
Grid.Column="2"
Margin="4,0,0,4"
Text="{x:Static lang:Resources.Label_Height}"/>
<ui:NumberBox
Grid.Row="1"
Grid.Column="2"
Margin="4,0,0,0"
PlaceholderText="128"
SmallChange="128"
Value="{Binding Height}"
ValidationMode="InvalidInputOverwritten"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Compact"/>
</Grid>
</sg:SpacedGrid>
<sg:SpacedGrid

12
StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml

@ -9,6 +9,9 @@
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:scroll="clr-namespace:StabilityMatrix.Avalonia.Controls.Scroll"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
x:DataType="vmInference:ImageFolderCardViewModel">
<Design.PreviewWith>
@ -18,6 +21,8 @@
</Grid>
</Panel>
</Design.PreviewWith>
<converters:FileUriConverter x:Key="FileUriConverter" />
<ControlTheme x:Key="{x:Type controls:ImageFolderCard}" TargetType="controls:ImageFolderCard">
<Setter Property="Background" Value="Transparent" />
@ -195,12 +200,11 @@
<Border ClipToBounds="True" CornerRadius="8">
<Grid RowDefinitions="*,Auto">
<controls:BetterAdvancedImage
<vendorLabs:BetterAsyncImage
Width="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Width}"
Height="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Height}"
Source="{Binding AbsolutePath}"
Stretch="UniformToFill"
StretchDirection="Both" />
Source="{Binding AbsolutePath, Converter={StaticResource FileUriConverter}}"
Stretch="UniformToFill" />
<Border
Grid.Row="1"

9
StabilityMatrix.Avalonia/Controls/Inference/ImageGalleryCard.axaml

@ -5,6 +5,8 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs"
x:DataType="vmInference:ImageGalleryCardViewModel">
<Design.PreviewWith>
@ -115,12 +117,11 @@
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:ImageSource}">
<controls:BetterAdvancedImage
<vendorLabs:BetterAsyncImage
MaxHeight="65"
CornerRadius="4"
Source="{Binding}"
Stretch="Uniform"
StretchDirection="Both" />
Source="{Binding Uri}"
Stretch="Uniform" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

48
StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml

@ -0,0 +1,48 @@
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
x:DataType="vmInference:LayerDiffuseCardViewModel">
<Design.PreviewWith>
<Panel Width="400" Height="200">
<StackPanel Width="300" VerticalAlignment="Center">
<controls:LayerDiffuseCard DataContext="{x:Static mocks:DesignData.LayerDiffuseCardViewModel}"/>
</StackPanel>
</Panel>
</Design.PreviewWith>
<Style Selector="controls|LayerDiffuseCard">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="12">
<sg:SpacedGrid
ColumnDefinitions="Auto,*"
ColumnSpacing="8"
RowDefinitions="*,*,*,*"
RowSpacing="0">
<!-- Mode Selection -->
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="Mode"
TextAlignment="Left" />
<ui:FAComboBox
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Stretch"
DisplayMemberBinding="{Binding Converter={x:Static converters:EnumAttributeConverters.DisplayName}}"
ItemsSource="{Binding AvailableModes}"
SelectedItem="{Binding SelectedMode}" />
</sg:SpacedGrid>
</controls:Card>
</ControlTemplate>
</Setter>
</Style>
</Styles>

7
StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml.cs

@ -0,0 +1,7 @@
using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class LayerDiffuseCard : TemplatedControl;

30
StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml

@ -8,6 +8,8 @@
xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
x:DataType="vmInference:PromptCardViewModel">
<Design.PreviewWith>
<Grid Width="460" Height="600">
@ -56,21 +58,29 @@
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="0,-2,0,0"
Padding="10,4"
VerticalAlignment="Top"
VerticalContentAlignment="Top"
icons:Attached.Icon="fa-solid fa-question"
Classes="transparent-full"
Command="{Binding ShowHelpDialogCommand}" />
<Panel>
<Button
x:Name="PART_HelpButton"
Margin="0,-2,0,0"
Padding="10,4"
VerticalAlignment="Top"
VerticalContentAlignment="Top"
icons:Attached.Icon="fa-solid fa-question"
Classes="transparent-full"
Command="{Binding ShowHelpDialogCommand}" />
<ui:TeachingTip
Target="{Binding #PART_HelpButton}"
Title="{x:Static lang:Resources.TeachingTip_InferencePromptHelpButton}"
IsOpen="{Binding IsHelpButtonTeachingTipOpen, Mode=TwoWay}"/>
</Panel>
<Button
Padding="8,4"
Command="{Binding DebugShowTokensCommand}"
Content="Show Tokens"
IsVisible="{Binding SharedState.IsDebugMode}" />
</StackPanel>
<Border

2
StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml.cs

@ -12,7 +12,7 @@ using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class PromptCard : TemplatedControl
public class PromptCard : TemplatedControlBase
{
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)

6
StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml

@ -1,7 +1,9 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls">
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:controls1="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
@ -38,7 +40,7 @@
CornerRadius="12"
Command="{TemplateBinding Command}"
CommandParameter="{TemplateBinding CommandParameter}">
<controls:BetterAdvancedImage
<vendorLabs:BetterAsyncImage
Stretch="UniformToFill"
CornerRadius="8"
ContextFlyout="{TemplateBinding ContextFlyout}"

8
StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs

@ -11,8 +11,10 @@ public class SelectableImageButton : Button
public static readonly StyledProperty<bool?> IsSelectedProperty =
ToggleButton.IsCheckedProperty.AddOwner<SelectableImageButton>();
public static readonly StyledProperty<string?> SourceProperty =
AdvancedImage.SourceProperty.AddOwner<SelectableImageButton>();
public static readonly StyledProperty<Uri?> SourceProperty = AvaloniaProperty.Register<
SelectableImageButton,
Uri?
>("Source");
public static readonly StyledProperty<double> ImageWidthProperty = AvaloniaProperty.Register<
SelectableImageButton,
@ -48,7 +50,7 @@ public class SelectableImageButton : Button
set => SetValue(IsSelectedProperty, value);
}
public string? Source
public Uri? Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);

20
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/AsyncImageFailedEventArgs.cs

@ -0,0 +1,20 @@
using System;
using Avalonia.Interactivity;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
public partial class BetterAsyncImage
{
public class AsyncImageFailedEventArgs : RoutedEventArgs
{
internal AsyncImageFailedEventArgs(Exception? errorException = null, string errorMessage = "")
: base(FailedEvent)
{
ErrorException = errorException;
ErrorMessage = errorMessage;
}
public Exception? ErrorException { get; private set; }
public string ErrorMessage { get; private set; }
}
}

42
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Events.cs

@ -0,0 +1,42 @@
using System;
using Avalonia.Interactivity;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
public partial class BetterAsyncImage
{
/// <summary>
/// Deines the <see cref="Opened"/> event
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> OpenedEvent = RoutedEvent.Register<
BetterAsyncImage,
RoutedEventArgs
>(nameof(Opened), RoutingStrategies.Bubble);
/// <summary>
/// Deines the <see cref="Failed"/> event
/// </summary>
public static readonly RoutedEvent<global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs> FailedEvent =
RoutedEvent.Register<
BetterAsyncImage,
global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs
>(nameof(Failed), RoutingStrategies.Bubble);
/// <summary>
/// Occurs when the image is successfully loaded.
/// </summary>
public event EventHandler<RoutedEventArgs>? Opened
{
add => AddHandler(OpenedEvent, value);
remove => RemoveHandler(OpenedEvent, value);
}
/// <summary>
/// Occurs when the image fails to load the uri provided.
/// </summary>
public event EventHandler<global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs>? Failed
{
add => AddHandler(FailedEvent, value);
remove => RemoveHandler(FailedEvent, value);
}
}

135
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Properties.cs

@ -0,0 +1,135 @@
using System;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Labs.Controls;
using Avalonia.Media;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
public partial class BetterAsyncImage
{
/// <summary>
/// Defines the <see cref="PlaceholderSource"/> property.
/// </summary>
public static readonly StyledProperty<IImage?> PlaceholderSourceProperty = AvaloniaProperty.Register<
BetterAsyncImage,
IImage?
>(nameof(PlaceholderSource));
/// <summary>
/// Defines the <see cref="Source"/> property.
/// </summary>
public static readonly StyledProperty<Uri?> SourceProperty = AvaloniaProperty.Register<
BetterAsyncImage,
Uri?
>(nameof(Source));
/// <summary>
/// Defines the <see cref="Stretch"/> property.
/// </summary>
public static readonly StyledProperty<Stretch> StretchProperty = AvaloniaProperty.Register<
BetterAsyncImage,
Stretch
>(nameof(Stretch), Stretch.Uniform);
/// <summary>
/// Defines the <see cref="PlaceholderStretch"/> property.
/// </summary>
public static readonly StyledProperty<Stretch> PlaceholderStretchProperty = AvaloniaProperty.Register<
BetterAsyncImage,
Stretch
>(nameof(PlaceholderStretch), Stretch.Uniform);
/// <summary>
/// Defines the <see cref="State"/> property.
/// </summary>
public static readonly DirectProperty<BetterAsyncImage, AsyncImageState> StateProperty =
AvaloniaProperty.RegisterDirect<BetterAsyncImage, AsyncImageState>(
nameof(State),
o => o.State,
(o, v) => o.State = v
);
/// <summary>
/// Defines the <see cref="ImageTransition"/> property.
/// </summary>
public static readonly StyledProperty<IPageTransition?> ImageTransitionProperty =
AvaloniaProperty.Register<BetterAsyncImage, IPageTransition?>(
nameof(ImageTransition),
new CrossFade(TimeSpan.FromSeconds(0.25))
);
/// <summary>
/// Defines the <see cref="IsCacheEnabled"/> property.
/// </summary>
public static readonly DirectProperty<BetterAsyncImage, bool> IsCacheEnabledProperty =
AvaloniaProperty.RegisterDirect<BetterAsyncImage, bool>(
nameof(IsCacheEnabled),
o => o.IsCacheEnabled,
(o, v) => o.IsCacheEnabled = v
);
private bool _isCacheEnabled;
/// <summary>
/// Gets or sets the placeholder image.
/// </summary>
public IImage? PlaceholderSource
{
get => GetValue(PlaceholderSourceProperty);
set => SetValue(PlaceholderSourceProperty, value);
}
/// <summary>
/// Gets or sets the uri pointing to the image resource
/// </summary>
public Uri? Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
/// <summary>
/// Gets or sets a value controlling how the image will be stretched.
/// </summary>
public Stretch Stretch
{
get { return GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
}
/// <summary>
/// Gets or sets a value controlling how the placeholder will be stretched.
/// </summary>
public Stretch PlaceholderStretch
{
get { return GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
}
/// <summary>
/// Gets the current loading state of the image.
/// </summary>
public AsyncImageState State
{
get => _state;
private set => SetAndRaise(StateProperty, ref _state, value);
}
/// <summary>
/// Gets or sets the transition to run when the image is loaded.
/// </summary>
public IPageTransition? ImageTransition
{
get => GetValue(ImageTransitionProperty);
set => SetValue(ImageTransitionProperty, value);
}
/// <summary>
/// Gets or sets whether to use cache for retrieved images
/// </summary>
public bool IsCacheEnabled
{
get => _isCacheEnabled;
set => SetAndRaise(IsCacheEnabledProperty, ref _isCacheEnabled, value);
}
}

248
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.cs

@ -0,0 +1,248 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Labs.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
using StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
/// <summary>
/// An image control that asynchronously retrieves an image using a <see cref="Uri"/>.
/// </summary>
[TemplatePart("PART_Image", typeof(Image))]
[TemplatePart("PART_PlaceholderImage", typeof(Image))]
public partial class BetterAsyncImage : TemplatedControl
{
protected Image? ImagePart { get; private set; }
protected Image? PlaceholderPart { get; private set; }
private bool _isInitialized;
private CancellationTokenSource? _tokenSource;
private AsyncImageState _state;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
ImagePart = e.NameScope.Get<Image>("PART_Image");
PlaceholderPart = e.NameScope.Get<Image>("PART_PlaceholderImage");
_tokenSource = new CancellationTokenSource();
_isInitialized = true;
if (Source != null)
{
SetSource(Source);
}
}
private async void SetSource(object? source)
{
if (!_isInitialized)
{
return;
}
_tokenSource?.Cancel();
_tokenSource = new CancellationTokenSource();
AttachSource(null);
if (source == null)
{
return;
}
State = AsyncImageState.Loading;
if (Source is IImage image)
{
AttachSource(image);
return;
}
if (Source == null)
{
return;
}
var uri = Source;
if (uri != null && uri.IsAbsoluteUri)
{
if (uri.Scheme == "http" || uri.Scheme == "https")
{
Bitmap? bitmap = null;
// Android doesn't allow network requests on the main thread, even though we are using async apis.
#if NET6_0_OR_GREATER
if (OperatingSystem.IsAndroid())
{
await Task.Run(async () =>
{
try
{
bitmap = await LoadImageAsync(uri, _tokenSource.Token);
}
catch (Exception ex)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
});
}
});
}
else
#endif
{
try
{
bitmap = await LoadImageAsync(uri, _tokenSource.Token);
}
catch (Exception ex)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
});
}
}
AttachSource(bitmap);
}
else if (uri.Scheme == "avares")
{
try
{
AttachSource(new Bitmap(AssetLoader.Open(uri)));
}
catch (Exception ex)
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
}
}
else if (uri.Scheme == "file" && File.Exists(uri.LocalPath))
{
// Added error handling here for local files
try
{
AttachSource(new Bitmap(uri.LocalPath));
}
catch (Exception ex)
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
}
}
else
{
RaiseEvent(
new AsyncImageFailedEventArgs(
new UriFormatException($"Uri has unsupported scheme. Uri:{source}")
)
);
}
}
else
{
RaiseEvent(
new AsyncImageFailedEventArgs(
new UriFormatException($"Relative paths aren't supported. Uri:{source}")
)
);
}
}
private void AttachSource(IImage? image)
{
if (ImagePart != null)
{
ImagePart.Source = image;
}
_tokenSource?.Cancel();
_tokenSource = new CancellationTokenSource();
if (image == null)
{
State = AsyncImageState.Unloaded;
ImageTransition?.Start(ImagePart, PlaceholderPart, true, _tokenSource.Token);
}
else if (image.Size != default)
{
State = AsyncImageState.Loaded;
ImageTransition?.Start(PlaceholderPart, ImagePart, true, _tokenSource.Token);
RaiseEvent(new RoutedEventArgs(OpenedEvent));
}
}
private async Task<Bitmap> LoadImageAsync(Uri? url, CancellationToken token)
{
if (await ProvideCachedResourceAsync(url, token) is { } bitmap)
{
return bitmap;
}
#if NET6_0_OR_GREATER
using var client = new HttpClient();
var stream = await client.GetStreamAsync(url, token).ConfigureAwait(false);
await using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream, token).ConfigureAwait(false);
#elif NETSTANDARD2_0
using var client = new HttpClient();
var response = await client.GetAsync(url, token).ConfigureAwait(false);
var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
#endif
memoryStream.Position = 0;
return new Bitmap(memoryStream);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == SourceProperty)
{
SetSource(Source);
}
}
protected virtual async Task<Bitmap?> ProvideCachedResourceAsync(Uri? imageUri, CancellationToken token)
{
if (IsCacheEnabled && imageUri != null)
{
return await ImageCache
.Instance.GetFromCacheAsync(imageUri, cancellationToken: token)
.ConfigureAwait(false);
}
return null;
}
}

565
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheBase.cs

@ -0,0 +1,565 @@
// Parts of this file was taken from Windows Community Toolkit CacheBase implementation
// https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp.UI/Cache/ImageCache.cs
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
internal abstract class CacheBase<T>
{
private class ConcurrentRequest
{
public Task<T?>? Task { get; set; }
public bool EnsureCachedCopy { get; set; }
}
private readonly SemaphoreSlim _cacheFolderSemaphore = new SemaphoreSlim(1);
private string? _baseFolder = null;
private string? _cacheFolderName = null;
private string? _cacheFolder = null;
private InMemoryStorage<T>? _inMemoryFileStorage = null;
private ConcurrentDictionary<string, ConcurrentRequest> _concurrentTasks =
new ConcurrentDictionary<string, ConcurrentRequest>();
private HttpClient? _httpClient = null;
/// <summary>
/// Initializes a new instance of the <see cref="CacheBase{T}"/> class.
/// </summary>
protected CacheBase()
{
var options = CacheOptions.Default;
CacheDuration = options?.CacheDuration ?? TimeSpan.FromDays(1);
_baseFolder = options?.BaseCachePath ?? null;
_inMemoryFileStorage = new InMemoryStorage<T>();
RetryCount = 1;
}
/// <summary>
/// Gets or sets the life duration of every cache entry.
/// </summary>
public TimeSpan CacheDuration { get; set; }
/// <summary>
/// Gets or sets the number of retries trying to ensure the file is cached.
/// </summary>
public uint RetryCount { get; set; }
/// <summary>
/// Gets or sets max in-memory item storage count
/// </summary>
public int MaxMemoryCacheCount
{
get { return _inMemoryFileStorage?.MaxItemCount ?? 0; }
set
{
if (_inMemoryFileStorage != null)
_inMemoryFileStorage.MaxItemCount = value;
}
}
/// <summary>
/// Gets instance of <see cref="HttpClient"/>
/// </summary>
protected HttpClient HttpClient
{
get
{
if (_httpClient == null)
{
var messageHandler = new HttpClientHandler();
_httpClient = new HttpClient(messageHandler);
}
return _httpClient;
}
}
/// <summary>
/// Initializes FileCache and provides root folder and cache folder name
/// </summary>
/// <param name="folder">Folder that is used as root for cache</param>
/// <param name="folderName">Cache folder name</param>
/// <param name="httpMessageHandler">instance of <see cref="HttpMessageHandler"/></param>
/// <returns>awaitable task</returns>
public virtual async Task InitializeAsync(
string? folder = null,
string? folderName = null,
HttpMessageHandler? httpMessageHandler = null
)
{
_baseFolder = folder;
_cacheFolderName = folderName;
_cacheFolder = await GetCacheFolderAsync().ConfigureAwait(false);
if (httpMessageHandler != null)
{
_httpClient = new HttpClient(httpMessageHandler);
}
}
/// <summary>
/// Clears all files in the cache
/// </summary>
/// <returns>awaitable task</returns>
public async Task ClearAsync()
{
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var files = Directory.EnumerateFiles(folder!);
await InternalClearAsync(files.Select(x => x as string)).ConfigureAwait(false);
_inMemoryFileStorage?.Clear();
}
/// <summary>
/// Clears file if it has expired
/// </summary>
/// <param name="duration">timespan to compute whether file has expired or not</param>
/// <returns>awaitable task</returns>
public Task ClearAsync(TimeSpan duration)
{
return RemoveExpiredAsync(duration);
}
/// <summary>
/// Removes cached files that have expired
/// </summary>
/// <param name="duration">Optional timespan to compute whether file has expired or not. If no value is supplied, <see cref="CacheDuration"/> is used.</param>
/// <returns>awaitable task</returns>
public async Task RemoveExpiredAsync(TimeSpan? duration = null)
{
TimeSpan expiryDuration = duration ?? CacheDuration;
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var files = Directory.EnumerateFiles(folder!);
var filesToDelete = new List<string>();
foreach (var file in files)
{
if (file == null)
{
continue;
}
if (await IsFileOutOfDateAsync(file, expiryDuration, false).ConfigureAwait(false))
{
filesToDelete.Add(file);
}
}
await InternalClearAsync(filesToDelete).ConfigureAwait(false);
_inMemoryFileStorage?.Clear(expiryDuration);
}
/// <summary>
/// Removed items based on uri list passed
/// </summary>
/// <param name="uriForCachedItems">Enumerable uri list</param>
/// <returns>awaitable Task</returns>
public async Task RemoveAsync(IEnumerable<Uri> uriForCachedItems)
{
if (uriForCachedItems == null || !uriForCachedItems.Any())
{
return;
}
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var files = Directory.EnumerateFiles(folder!);
var filesToDelete = new List<string>();
var keys = new List<string>();
Dictionary<string, string> hashDictionary = new Dictionary<string, string>();
foreach (var file in files)
{
hashDictionary.Add(Path.GetFileName(file), file);
}
foreach (var uri in uriForCachedItems)
{
string fileName = GetCacheFileName(uri);
if (hashDictionary.TryGetValue(fileName, out var file))
{
filesToDelete.Add(file);
keys.Add(fileName);
}
}
await InternalClearAsync(filesToDelete).ConfigureAwait(false);
_inMemoryFileStorage?.Remove(keys);
}
/// <summary>
/// Assures that item represented by Uri is cached.
/// </summary>
/// <param name="uri">Uri of the item</param>
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be cached</param>
/// <param name="storeToMemoryCache">Indicates if item should be loaded into the in-memory storage</param>
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param>
/// <returns>Awaitable Task</returns>
public Task PreCacheAsync(
Uri uri,
bool throwOnError = false,
bool storeToMemoryCache = false,
CancellationToken cancellationToken = default(CancellationToken)
)
{
return GetItemAsync(uri, throwOnError, !storeToMemoryCache, cancellationToken);
}
/// <summary>
/// Retrieves item represented by Uri from the cache. If the item is not found in the cache, it will try to downloaded and saved before returning it to the caller.
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be found / downloaded.</param>
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param>
/// <returns>an instance of Generic type</returns>
public Task<T?> GetFromCacheAsync(
Uri uri,
bool throwOnError = false,
CancellationToken cancellationToken = default(CancellationToken)
)
{
return GetItemAsync(uri, throwOnError, false, cancellationToken);
}
/// <summary>
/// Gets the string containing cached item for given Uri
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>a string</returns>
public async Task<string> GetFileFromCacheAsync(Uri uri)
{
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
return Path.Combine(folder!, GetCacheFileName(uri));
}
/// <summary>
/// Retrieves item represented by Uri from the in-memory cache if it exists and is not out of date. If item is not found or is out of date, default instance of the generic type is returned.
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>an instance of Generic type</returns>
public T? GetFromMemoryCache(Uri uri)
{
T? instance = default(T);
string fileName = GetCacheFileName(uri);
if (_inMemoryFileStorage?.MaxItemCount > 0)
{
var msi = _inMemoryFileStorage?.GetItem(fileName, CacheDuration);
if (msi != null)
{
instance = msi.Item;
}
}
return instance;
}
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="stream">input stream</param>
/// <returns>awaitable task</returns>
protected abstract Task<T> ConvertFromAsync(Stream stream);
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="baseFile">storage file</param>
/// <returns>awaitable task</returns>
protected abstract Task<T> ConvertFromAsync(string baseFile);
/// <summary>
/// Override-able method that checks whether file is valid or not.
/// </summary>
/// <param name="file">storage file</param>
/// <param name="duration">cache duration</param>
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param>
/// <returns>bool indicate whether file has expired or not</returns>
protected virtual async Task<bool> IsFileOutOfDateAsync(
string file,
TimeSpan duration,
bool treatNullFileAsOutOfDate = true
)
{
if (file == null)
{
return treatNullFileAsOutOfDate;
}
var info = new FileInfo(file);
return info.Length == 0 || DateTime.Now.Subtract(info.LastWriteTime) > duration;
}
private static string GetCacheFileName(Uri uri)
{
return CreateHash64(uri.ToString()).ToString();
}
private static ulong CreateHash64(string str)
{
byte[] utf8 = System.Text.Encoding.UTF8.GetBytes(str);
ulong value = (ulong)utf8.Length;
for (int n = 0; n < utf8.Length; n++)
{
value += (ulong)utf8[n] << ((n * 5) % 56);
}
return value;
}
private async Task<T?> GetItemAsync(
Uri uri,
bool throwOnError,
bool preCacheOnly,
CancellationToken cancellationToken
)
{
T? instance = default(T);
string fileName = GetCacheFileName(uri);
_concurrentTasks.TryGetValue(fileName, out var request);
// if similar request exists check if it was preCacheOnly and validate that current request isn't preCacheOnly
if (request != null && request.EnsureCachedCopy && !preCacheOnly)
{
if (request.Task != null)
await request.Task.ConfigureAwait(false);
request = null;
}
if (request == null)
{
request = new ConcurrentRequest()
{
Task = GetFromCacheOrDownloadAsync(uri, fileName, preCacheOnly, cancellationToken),
EnsureCachedCopy = preCacheOnly
};
_concurrentTasks[fileName] = request;
}
try
{
if (request.Task != null)
instance = await request.Task.ConfigureAwait(false);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
if (throwOnError)
{
throw;
}
}
finally
{
_concurrentTasks.TryRemove(fileName, out _);
}
return instance;
}
private async Task<T?> GetFromCacheOrDownloadAsync(
Uri uri,
string fileName,
bool preCacheOnly,
CancellationToken cancellationToken
)
{
T? instance = default(T);
if (_inMemoryFileStorage?.MaxItemCount > 0)
{
var msi = _inMemoryFileStorage?.GetItem(fileName, CacheDuration);
if (msi != null)
{
instance = msi.Item;
}
}
if (instance != null)
{
return instance;
}
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var baseFile = Path.Combine(folder!, fileName);
bool downloadDataFile =
!File.Exists(baseFile)
|| await IsFileOutOfDateAsync(baseFile, CacheDuration).ConfigureAwait(false);
if (!File.Exists(baseFile))
{
File.Create(baseFile).Dispose();
}
if (downloadDataFile)
{
uint retries = 0;
try
{
while (retries < RetryCount)
{
try
{
instance = await DownloadFileAsync(uri, baseFile, preCacheOnly, cancellationToken)
.ConfigureAwait(false);
if (instance != null)
{
break;
}
}
catch (FileNotFoundException) { }
retries++;
}
}
catch (Exception ex)
{
File.Delete(baseFile);
throw; // re-throwing the exception changes the stack trace. just throw
}
}
if (EqualityComparer<T>.Default.Equals(instance, default(T)) && !preCacheOnly)
{
instance = await ConvertFromAsync(baseFile).ConfigureAwait(false);
if (_inMemoryFileStorage?.MaxItemCount > 0)
{
var properties = new FileInfo(baseFile);
var msi = new InMemoryStorageItem<T>(fileName, properties.LastWriteTime, instance);
_inMemoryFileStorage?.SetItem(msi);
}
}
return instance;
}
private async Task<T?> DownloadFileAsync(
Uri uri,
string baseFile,
bool preCacheOnly,
CancellationToken cancellationToken
)
{
T? instance = default(T);
using (MemoryStream ms = new MemoryStream())
{
using (var stream = await HttpClient.GetStreamAsync(uri))
{
stream.CopyTo(ms);
ms.Flush();
ms.Position = 0;
using (var fs = File.Open(baseFile, FileMode.OpenOrCreate, FileAccess.Write))
{
ms.CopyTo(fs);
fs.Flush();
ms.Position = 0;
}
}
// if its pre-cache we aren't looking to load items in memory
if (!preCacheOnly)
{
instance = await ConvertFromAsync(ms).ConfigureAwait(false);
}
}
return instance;
}
private async Task InternalClearAsync(IEnumerable<string?> files)
{
foreach (var file in files)
{
try
{
File.Delete(file!);
}
catch
{
// Just ignore errors for now}
}
}
}
/// <summary>
/// Initializes with default values if user has not initialized explicitly
/// </summary>
/// <returns>awaitable task</returns>
private async Task ForceInitialiseAsync()
{
if (_cacheFolder != null)
{
return;
}
await _cacheFolderSemaphore.WaitAsync().ConfigureAwait(false);
_inMemoryFileStorage = new InMemoryStorage<T>();
if (_baseFolder == null)
{
_baseFolder = Path.GetTempPath();
}
if (string.IsNullOrWhiteSpace(_cacheFolderName))
{
_cacheFolderName = GetType().Name;
}
try
{
_cacheFolder = Path.Combine(_baseFolder, _cacheFolderName);
Directory.CreateDirectory(_cacheFolder);
}
finally
{
_cacheFolderSemaphore.Release();
}
}
private async Task<string?> GetCacheFolderAsync()
{
if (_cacheFolder == null)
{
await ForceInitialiseAsync().ConfigureAwait(false);
}
return _cacheFolder;
}
}

18
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheOptions.cs

@ -0,0 +1,18 @@
using System;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
public class CacheOptions
{
private static CacheOptions? _cacheOptions;
public static CacheOptions Default => _cacheOptions ??= new CacheOptions();
public static void SetDefault(CacheOptions defaultCacheOptions)
{
_cacheOptions = defaultCacheOptions;
}
public string? BaseCachePath { get; set; }
public TimeSpan? CacheDuration { get; set; }
}

36
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/FileCache.cs

@ -0,0 +1,36 @@
using System.IO;
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Provides methods and tools to cache files in a folder
/// </summary>
internal class FileCache : CacheBase<string>
{
/// <summary>
/// Private singleton field.
/// </summary>
private static FileCache? _instance;
/// <summary>
/// Gets public singleton property.
/// </summary>
public static FileCache Instance => _instance ?? (_instance = new FileCache());
protected override Task<string> ConvertFromAsync(Stream stream)
{
// nothing to do in this instance;
return Task.FromResult<string>("");
}
/// <summary>
/// Returns a cached path
/// </summary>
/// <param name="baseFile">storage file</param>
/// <returns>awaitable task</returns>
protected override Task<string> ConvertFromAsync(string baseFile)
{
return Task.FromResult(baseFile);
}
}

76
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/ImageCache.cs

@ -0,0 +1,76 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Provides methods and tools to cache images in a folder
/// </summary>
internal class ImageCache : CacheBase<Bitmap>
{
/// <summary>
/// Private singleton field.
/// </summary>
[ThreadStatic]
private static ImageCache? _instance;
/// <summary>
/// Gets public singleton property.
/// </summary>
public static ImageCache Instance => _instance ?? (_instance = new ImageCache());
/// <summary>
/// Creates a bitmap from a stream
/// </summary>
/// <param name="stream">input stream</param>
/// <returns>awaitable task</returns>
protected override async Task<Bitmap> ConvertFromAsync(Stream stream)
{
if (stream.Length == 0)
{
throw new FileNotFoundException();
}
return new Bitmap(stream);
}
/// <summary>
/// Creates a bitmap from a cached file
/// </summary>
/// <param name="baseFile">file</param>
/// <returns>awaitable task</returns>
protected override async Task<Bitmap> ConvertFromAsync(string baseFile)
{
using (var stream = File.OpenRead(baseFile))
{
return await ConvertFromAsync(stream).ConfigureAwait(false);
}
}
/// <summary>
/// Checks whether file is valid or not.
/// </summary>
/// <param name="file">file</param>
/// <param name="duration">cache duration</param>
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param>
/// <returns>bool indicate whether file has expired or not</returns>
protected override async Task<bool> IsFileOutOfDateAsync(
string file,
TimeSpan duration,
bool treatNullFileAsOutOfDate = true
)
{
if (file == null)
{
return treatNullFileAsOutOfDate;
}
var fileInfo = new FileInfo(file);
return fileInfo.Length == 0
|| DateTime.Now.Subtract(File.GetLastAccessTime(file)) > duration
|| DateTime.Now.Subtract(File.GetLastWriteTime(file)) > duration;
}
}

156
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorage.cs

@ -0,0 +1,156 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Labs.Controls.Cache;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Generic in-memory storage of items
/// </summary>
/// <typeparam name="T">T defines the type of item stored</typeparam>
public class InMemoryStorage<T>
{
private int _maxItemCount;
private ConcurrentDictionary<string, InMemoryStorageItem<T>> _inMemoryStorage =
new ConcurrentDictionary<string, InMemoryStorageItem<T>>();
private object _settingMaxItemCountLocker = new object();
/// <summary>
/// Gets or sets the maximum count of Items that can be stored in this InMemoryStorage instance.
/// </summary>
public int MaxItemCount
{
get { return _maxItemCount; }
set
{
if (_maxItemCount == value)
{
return;
}
_maxItemCount = value;
lock (_settingMaxItemCountLocker)
{
EnsureStorageBounds(value);
}
}
}
/// <summary>
/// Clears all items stored in memory
/// </summary>
public void Clear()
{
_inMemoryStorage.Clear();
}
/// <summary>
/// Clears items stored in memory based on duration passed
/// </summary>
/// <param name="duration">TimeSpan to identify expired items</param>
public void Clear(TimeSpan duration)
{
var expirationDate = DateTime.Now.Subtract(duration);
var itemsToRemove = _inMemoryStorage
.Where(kvp => kvp.Value.LastUpdated <= expirationDate)
.Select(kvp => kvp.Key);
if (itemsToRemove.Any())
{
Remove(itemsToRemove);
}
}
/// <summary>
/// Remove items based on provided keys
/// </summary>
/// <param name="keys">identified of the in-memory storage item</param>
public void Remove(IEnumerable<string> keys)
{
foreach (var key in keys)
{
if (string.IsNullOrWhiteSpace(key))
{
continue;
}
_inMemoryStorage.TryRemove(key, out _);
}
}
/// <summary>
/// Add new item to in-memory storage
/// </summary>
/// <param name="item">item to be stored</param>
public void SetItem(InMemoryStorageItem<T> item)
{
if (MaxItemCount == 0)
{
return;
}
_inMemoryStorage[item.Id] = item;
// ensure max limit is maintained. trim older entries first
if (_inMemoryStorage.Count > MaxItemCount)
{
var itemsToRemove = _inMemoryStorage
.OrderBy(kvp => kvp.Value.Created)
.Take(_inMemoryStorage.Count - MaxItemCount)
.Select(kvp => kvp.Key);
Remove(itemsToRemove);
}
}
/// <summary>
/// Get item from in-memory storage as long as it has not ex
/// </summary>
/// <param name="id">id of the in-memory storage item</param>
/// <param name="duration">timespan denoting expiration</param>
/// <returns>Valid item if not out of date or return null if out of date or item does not exist</returns>
public InMemoryStorageItem<T>? GetItem(string id, TimeSpan duration)
{
if (!_inMemoryStorage.TryGetValue(id, out var tempItem))
{
return null;
}
var expirationDate = DateTime.Now.Subtract(duration);
if (tempItem.LastUpdated > expirationDate)
{
return tempItem;
}
_inMemoryStorage.TryRemove(id, out _);
return null;
}
private void EnsureStorageBounds(int maxCount)
{
if (_inMemoryStorage.Count == 0)
{
return;
}
if (maxCount == 0)
{
_inMemoryStorage.Clear();
return;
}
if (_inMemoryStorage.Count > maxCount)
{
Remove(_inMemoryStorage.Keys.Take(_inMemoryStorage.Count - maxCount));
}
}
}

49
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorageItem.cs

@ -0,0 +1,49 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Generic InMemoryStorageItem holds items for InMemoryStorage.
/// </summary>
/// <typeparam name="T">Type is set by consuming cache</typeparam>
public class InMemoryStorageItem<T>
{
/// <summary>
/// Gets the item identifier
/// </summary>
public string Id { get; private set; }
/// <summary>
/// Gets the item created timestamp.
/// </summary>
public DateTime Created { get; private set; }
/// <summary>
/// Gets the item last updated timestamp.
/// </summary>
public DateTime LastUpdated { get; private set; }
/// <summary>
/// Gets the item being stored.
/// </summary>
public T Item { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryStorageItem{T}"/> class.
/// Constructor for InMemoryStorageItem
/// </summary>
/// <param name="id">uniquely identifies the item</param>
/// <param name="lastUpdated">last updated timestamp</param>
/// <param name="item">the item being stored</param>
public InMemoryStorageItem(string id, DateTime lastUpdated, T item)
{
Id = id;
LastUpdated = lastUpdated;
Item = item;
Created = DateTime.Now;
}
}

21
StabilityMatrix.Avalonia/Controls/VendorLabs/LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 AvaloniaUI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

48
StabilityMatrix.Avalonia/Controls/VendorLabs/Themes/BetterAsyncImage.axaml

@ -0,0 +1,48 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs">
<Design.PreviewWith>
<Border Width="200"
Height="200">
</Border>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type vendorLabs:BetterAsyncImage}"
TargetType="vendorLabs:BetterAsyncImage">
<Setter Property="Background" Value="Transparent" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Border Margin="0"
Padding="0"
ClipToBounds="True"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Image Name="PART_PlaceholderImage"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Source="{TemplateBinding PlaceholderSource}"
Stretch="{TemplateBinding PlaceholderStretch}"/>
<Image Name="PART_Image"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Stretch="{TemplateBinding Stretch}"/>
</Grid>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^[State=Failed] /template/ Image#PART_Image">
<Setter Property="Opacity"
Value="0.0" />
</Style>
<Style Selector="^[State=Failed] /template/ Image#PART_PlaceholderImage">
<Setter Property="Opacity"
Value="1.0" />
</Style>
</ControlTheme>
</ResourceDictionary>

1
StabilityMatrix.Avalonia/Converters/FileUriConverter.cs

@ -17,6 +17,7 @@ public class FileUriConverter : IValueConverter
return value switch
{
string str when str.StartsWith("avares://") => new Uri(str),
string str => new Uri("file://" + str),
IFormattable formattable => new Uri("file://" + formattable.ToString(null, culture)),
_ => null

119
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -36,6 +36,7 @@ using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.OpenArt;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
@ -51,6 +52,7 @@ using HuggingFacePageViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointB
namespace StabilityMatrix.Avalonia.DesignData;
[Localizable(false)]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public static class DesignData
{
@ -98,6 +100,19 @@ public static class DesignData
},
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
},
new()
{
Id = Guid.NewGuid(),
DisplayName = "Running Comfy",
PackageName = "ComfyUI",
Version = new InstalledPackageVersion
{
InstalledBranch = "master",
InstalledCommitSha = "abc12uwu345568972abaedf7g7e679a98879e879f87ga8"
},
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
}
},
ActiveInstalledPackageId = activePackageId
@ -312,7 +327,7 @@ public static class DesignData
);
}*/
CivitAiBrowserViewModel.ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>
CivitAiBrowserViewModel.ModelCards = new ObservableCollectionExtended<CheckpointBrowserCardViewModel>
{
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
@ -345,7 +360,7 @@ public static class DesignData
{
new()
{
Nsfw = "None",
NsfwLevel = 1,
Url =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/"
+ "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg"
@ -404,7 +419,8 @@ public static class DesignData
new PackageInstallProgressItemViewModel(
new PackageModificationRunner
{
CurrentProgress = new ProgressReport(0.5f, "Installing package...")
CurrentProgress = new ProgressReport(0.5f, "Installing package..."),
ModificationCompleteMessage = "Package installed successfully"
}
)
}
@ -507,6 +523,16 @@ public static class DesignData
}
)
};
vm.Categories = new ObservableCollectionExtended<PackageOutputCategory>
{
new()
{
Name = "Category 1",
Path = "path1",
SubDirectories = [new PackageOutputCategory { Name = "SubCategory 1", Path = "path3" }]
},
new() { Name = "Category 2", Path = "path2" }
};
return vm;
}
}
@ -897,6 +923,39 @@ The gallery images are often inpainted, but you will get something very similar
vm.IsBatchIndexEnabled = true;
});
public static LayerDiffuseCardViewModel LayerDiffuseCardViewModel =>
DialogFactory.Get<LayerDiffuseCardViewModel>();
public static InstalledWorkflowsViewModel InstalledWorkflowsViewModel
{
get
{
var vm = Services.GetRequiredService<InstalledWorkflowsViewModel>();
vm.DisplayedWorkflows = new ObservableCollectionExtended<OpenArtMetadata>
{
new()
{
Workflow = new()
{
Name = "Test Workflow",
Creator = new OpenArtCreator { Name = "Test Creator" },
Thumbnails =
[
new OpenArtThumbnail
{
Url = new Uri(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dd9b038c-bd15-43ab-86ab-66e145ad7ff2/width=512"
)
}
]
}
}
};
return vm;
}
}
public static IList<ICompletionData> SampleCompletionData =>
new List<ICompletionData>
{
@ -1008,6 +1067,60 @@ The gallery images are often inpainted, but you will get something very similar
public static ControlNetCardViewModel ControlNetCardViewModel =>
DialogFactory.Get<ControlNetCardViewModel>();
public static OpenArtWorkflowViewModel OpenArtWorkflowViewModel =>
new(Services.GetRequiredService<ISettingsManager>(), Services.GetRequiredService<IPackageFactory>())
{
Workflow = new OpenArtSearchResult
{
Name = "Test Workflow",
Creator = new OpenArtCreator
{
Name = "Test Creator Name",
Username = "Test Creator Username"
},
Thumbnails =
[
new OpenArtThumbnail
{
Url = new Uri(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500"
)
}
],
NodesIndex =
[
"Anything Everywhere",
"Reroute",
"Note",
".",
"ComfyUI's ControlNet Auxiliary Preprocessors",
"DWPreprocessor",
"PixelPerfectResolution",
"AIO_Preprocessor",
",",
"ComfyUI",
"PreviewImage",
"CLIPTextEncode",
"EmptyLatentImage",
"SplitImageWithAlpha",
"ControlNetApplyAdvanced",
"JoinImageWithAlpha",
"LatentUpscaleBy",
"VAEEncode",
"LoadImage",
"ControlNetLoader",
"CLIPVisionLoader",
"SaveImage",
",",
"ComfyUI Impact Pack",
"SAMLoader",
"UltralyticsDetectorProvider",
"FaceDetailer",
","
]
}
};
public static string CurrentDirectory => Directory.GetCurrentDirectory();
public static Indexer Types { get; } = new();

3
StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs

@ -41,6 +41,9 @@ public partial class MockInferenceClientManager : ObservableObject, IInferenceCl
public IObservableCollection<ComfyScheduler> Schedulers { get; } =
new ObservableCollectionExtended<ComfyScheduler>(ComfyScheduler.Defaults);
public IObservableCollection<ComfyAuxPreprocessor> Preprocessors { get; } =
new ObservableCollectionExtended<ComfyAuxPreprocessor>(ComfyAuxPreprocessor.Defaults);
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanUserConnect))]
private bool isConnected;

93
StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs

@ -20,17 +20,15 @@ public static class ComfyNodeBuilderExtensions
int? batchIndex = null
)
{
var emptyLatent = builder
.Nodes
.AddTypedNode(
new ComfyNodeBuilder.EmptyLatentImage
{
Name = "EmptyLatentImage",
BatchSize = batchSize,
Height = height,
Width = width
}
);
var emptyLatent = builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.EmptyLatentImage
{
Name = "EmptyLatentImage",
BatchSize = batchSize,
Height = height,
Width = width
}
);
builder.Connections.Primary = emptyLatent.Output;
builder.Connections.PrimarySize = new Size(width, height);
@ -39,15 +37,15 @@ public static class ComfyNodeBuilderExtensions
if (batchIndex is not null)
{
builder.Connections.Primary = builder
.Nodes
.AddNamedNode(
ComfyNodeBuilder.LatentFromBatch(
"LatentFromBatch",
builder.GetPrimaryAsLatent(),
.Nodes.AddTypedNode(
new ComfyNodeBuilder.LatentFromBatch
{
Name = "LatentFromBatch",
Samples = builder.GetPrimaryAsLatent(),
// remote expects a 0-based index, vm is 1-based
batchIndex.Value - 1,
1
)
BatchIndex = batchIndex.Value - 1,
Length = 1
}
)
.Output;
}
@ -67,9 +65,9 @@ public static class ComfyNodeBuilderExtensions
var sourceImageRelativePath = Path.Combine("Inference", image.GetHashGuidFileNameCached());
// Load source
var loadImage = builder
.Nodes
.AddTypedNode(new ComfyNodeBuilder.LoadImage { Name = "LoadImage", Image = sourceImageRelativePath });
var loadImage = builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.LoadImage { Name = "LoadImage", Image = sourceImageRelativePath }
);
builder.Connections.Primary = loadImage.Output1;
builder.Connections.PrimarySize = imageSize;
@ -78,15 +76,15 @@ public static class ComfyNodeBuilderExtensions
if (batchIndex is not null)
{
builder.Connections.Primary = builder
.Nodes
.AddNamedNode(
ComfyNodeBuilder.LatentFromBatch(
"LatentFromBatch",
builder.GetPrimaryAsLatent(),
.Nodes.AddTypedNode(
new ComfyNodeBuilder.LatentFromBatch
{
Name = "LatentFromBatch",
Samples = builder.GetPrimaryAsLatent(),
// remote expects a 0-based index, vm is 1-based
batchIndex.Value - 1,
1
)
BatchIndex = batchIndex.Value - 1,
Length = 1
}
)
.Output;
}
@ -97,25 +95,24 @@ public static class ComfyNodeBuilderExtensions
if (builder.Connections.Primary is null)
throw new ArgumentException("No Primary");
var image = builder
.Connections
.Primary
.Match(
_ =>
builder.GetPrimaryAsImage(
builder.Connections.PrimaryVAE
?? builder.Connections.Refiner.VAE
?? builder.Connections.Base.VAE
?? throw new ArgumentException("No Primary, Refiner, or Base VAE")
),
image => image
);
var image = builder.Connections.Primary.Match(
_ =>
builder.GetPrimaryAsImage(
builder.Connections.PrimaryVAE
?? builder.Connections.Refiner.VAE
?? builder.Connections.Base.VAE
?? throw new ArgumentException("No Primary, Refiner, or Base VAE")
),
image => image
);
var previewImage = builder
.Nodes
.AddTypedNode(
new ComfyNodeBuilder.PreviewImage { Name = builder.Nodes.GetUniqueName("SaveImage"), Images = image }
);
var previewImage = builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.PreviewImage
{
Name = builder.Nodes.GetUniqueName("SaveImage"),
Images = image
}
);
builder.Connections.OutputNodes.Add(previewImage);

14
StabilityMatrix.Avalonia/Extensions/DataObjectExtensions.cs

@ -1,4 +1,5 @@
using Avalonia.Input;
using System.Runtime.InteropServices;
using Avalonia.Input;
namespace StabilityMatrix.Avalonia.Extensions;
@ -9,9 +10,16 @@ public static class DataObjectExtensions
/// </summary>
public static T? GetContext<T>(this IDataObject dataObject)
{
if (dataObject.Get("Context") is T context)
try
{
return context;
if (dataObject.Get("Context") is T context)
{
return context;
}
}
catch (COMException)
{
return default;
}
return default;

9
StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs

@ -103,4 +103,13 @@ public class FallbackRamCachedWebImageLoader : RamCachedWebImageLoader
}
}
}
public void ClearCache()
{
var cache =
this.GetPrivateField<ConcurrentDictionary<string, Task<Bitmap?>>>("_memoryCache")
?? throw new NullReferenceException("Memory cache not found");
cache.Clear();
}
}

2
StabilityMatrix.Avalonia/Helpers/UriHandler.cs

@ -83,7 +83,7 @@ public class UriHandler
[SupportedOSPlatform("windows")]
private void RegisterUriSchemeWin()
{
using var key = Registry.CurrentUser.CreateSubKey(@$"SOFTWARE\Classes\{Scheme}");
using var key = Registry.CurrentUser.CreateSubKey(@$"SOFTWARE\Classes\{Scheme}", true);
key.SetValue("", "URL:" + Description);
key.SetValue(null, Description);

153
StabilityMatrix.Avalonia/Languages/Resources.Designer.cs generated

@ -374,6 +374,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Open on OpenArt.
/// </summary>
public static string Action_OpenOnOpenArt {
get {
return ResourceManager.GetString("Action_OpenOnOpenArt", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open Project....
/// </summary>
@ -734,6 +743,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Another instance of Stability Matrix is already running. Please close it before starting a new one..
/// </summary>
public static string Label_AnotherInstanceAlreadyRunning {
get {
return ResourceManager.GetString("Label_AnotherInstanceAlreadyRunning", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to API Key.
/// </summary>
@ -1301,6 +1319,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Error retrieving workflows.
/// </summary>
public static string Label_ErrorRetrievingWorkflows {
get {
return ResourceManager.GetString("Label_ErrorRetrievingWorkflows", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Everything looks good!.
/// </summary>
@ -1337,6 +1364,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Finished importing workflow and custom nodes.
/// </summary>
public static string Label_FinishedImportingWorkflow {
get {
return ResourceManager.GetString("Label_FinishedImportingWorkflow", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to First Page.
/// </summary>
@ -1481,6 +1517,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Infinite Scrolling.
/// </summary>
public static string Label_InfiniteScrolling {
get {
return ResourceManager.GetString("Label_InfiniteScrolling", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Inner exception.
/// </summary>
@ -1769,6 +1814,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Node Details.
/// </summary>
public static string Label_NodeDetails {
get {
return ResourceManager.GetString("Label_NodeDetails", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No extensions found..
/// </summary>
@ -1832,6 +1886,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to OpenArt Browser.
/// </summary>
public static string Label_OpenArtBrowser {
get {
return ResourceManager.GetString("Label_OpenArtBrowser", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output Folder.
/// </summary>
@ -2273,6 +2336,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Stability Matrix is already running.
/// </summary>
public static string Label_StabilityMatrixAlreadyRunning {
get {
return ResourceManager.GetString("Label_StabilityMatrixAlreadyRunning", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Steps.
/// </summary>
@ -2543,6 +2615,69 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Workflow Browser.
/// </summary>
public static string Label_WorkflowBrowser {
get {
return ResourceManager.GetString("Label_WorkflowBrowser", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Workflow Deleted.
/// </summary>
public static string Label_WorkflowDeleted {
get {
return ResourceManager.GetString("Label_WorkflowDeleted", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} deleted successfully.
/// </summary>
public static string Label_WorkflowDeletedSuccessfully {
get {
return ResourceManager.GetString("Label_WorkflowDeletedSuccessfully", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Workflow Description.
/// </summary>
public static string Label_WorkflowDescription {
get {
return ResourceManager.GetString("Label_WorkflowDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The workflow and custom nodes have been imported..
/// </summary>
public static string Label_WorkflowImportComplete {
get {
return ResourceManager.GetString("Label_WorkflowImportComplete", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Workflow Imported.
/// </summary>
public static string Label_WorkflowImported {
get {
return ResourceManager.GetString("Label_WorkflowImported", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Workflows.
/// </summary>
public static string Label_Workflows {
get {
return ResourceManager.GetString("Label_Workflows", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You&apos;re up to date.
/// </summary>
@ -2624,6 +2759,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Installed Workflows.
/// </summary>
public static string TabLabel_InstalledWorkflows {
get {
return ResourceManager.GetString("TabLabel_InstalledWorkflows", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Add a package to get started!.
/// </summary>
@ -2651,6 +2795,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Click here to review prompt syntax and how to include Lora / Embeddings..
/// </summary>
public static string TeachingTip_InferencePromptHelpButton {
get {
return ResourceManager.GetString("TeachingTip_InferencePromptHelpButton", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Additional folders such as IPAdapters and TextualInversions (embeddings) can be enabled here.
/// </summary>

116
StabilityMatrix.Avalonia/Languages/Resources.es.resx

@ -963,4 +963,120 @@
<data name="Label_RecommendedModelsSubText" xml:space="preserve">
<value>Mientras se instala tu paquete, aquí hay algunos modelos que recomendamos para ayudarte a comenzar.</value>
</data>
<data name="Label_Notifications" xml:space="preserve">
<value>Notificaciones</value>
</data>
<data name="Label_NotificationOption_None" xml:space="preserve">
<value>Ninguno</value>
</data>
<data name="Label_ComfyRequiredTitle" xml:space="preserve">
<value>Se Requiere ComfyUI</value>
</data>
<data name="Label_ComfyRequiredDetail" xml:space="preserve">
<value>Se requiere ComfyUI para instalar este paquete. ¿Quieres instalarlo ahora?</value>
</data>
<data name="Error_PleaseSelectDownloadLocation" xml:space="preserve">
<value>Por favor, seleccione una ubicación de descarga.</value>
</data>
<data name="Label_SelectDownloadLocation" xml:space="preserve">
<value>Seleccione la Ubicación de Descarga:</value>
</data>
<data name="Label_Config" xml:space="preserve">
<value>Configuración</value>
<comment>This is used in inference when models have a yaml config file</comment>
</data>
<data name="Label_ToggleAutoScrolling" xml:space="preserve">
<value>Desplazamiento Automático hasta el Final</value>
</data>
<data name="Label_ConfirmExit" xml:space="preserve">
<value>Confirmar Salida</value>
</data>
<data name="Label_ConfirmExitDetail" xml:space="preserve">
<value>¿Seguro que quieres salir? Esto también cerrará cualquier paquete que se esté ejecutando actualmente.</value>
</data>
<data name="Label_Console" xml:space="preserve">
<value>Consola</value>
</data>
<data name="Label_WebUi" xml:space="preserve">
<value>Interfaz Web</value>
<comment>This will be used on a button to launch the web ui</comment>
</data>
<data name="Label_Packages" xml:space="preserve">
<value>Paquetes</value>
</data>
<data name="Label_ActionCannotBeUndone" xml:space="preserve">
<value>Esta acción no se puede deshacer.</value>
</data>
<data name="Label_AreYouSureDeleteImages" xml:space="preserve">
<value>¿Estás seguro de que deseas eliminar {0} imágenes?</value>
</data>
<data name="Label_CheckingHardware" xml:space="preserve">
<value>Estamos verificando algunas especificaciones de hardware para determinar la compatibilidad.</value>
</data>
<data name="Label_EverythingLooksGood" xml:space="preserve">
<value>¡Todo parece estar bien!</value>
</data>
<data name="Label_NvidiaGpuRecommended" xml:space="preserve">
<value>Recomendamos una GPU con soporte CUDA para obtener la mejor experiencia. Puedes continuar sin una, pero es posible que algunos paquetes no funcionen y la inferencia pueda ser más lenta.</value>
</data>
<data name="Label_Checkpoints" xml:space="preserve">
<value>Checkpoints</value>
</data>
<data name="Label_ModelBrowser" xml:space="preserve">
<value>Navegador de Modelos</value>
</data>
<data name="Label_Workflows" xml:space="preserve">
<value>Workflows</value>
</data>
<data name="Label_InfiniteScrolling" xml:space="preserve">
<value>Scroll Infinito</value>
</data>
<data name="Label_WorkflowBrowser" xml:space="preserve">
<value>Navegador de Workflows</value>
</data>
<data name="Action_OpenOnOpenArt" xml:space="preserve">
<value>Abrir en OpenArt</value>
</data>
<data name="Label_NodeDetails" xml:space="preserve">
<value>Detalles del Nodo</value>
</data>
<data name="Label_WorkflowDescription" xml:space="preserve">
<value>Descripción del Workflow</value>
</data>
<data name="Label_OpenArtBrowser" xml:space="preserve">
<value>Navegador OpenArt</value>
</data>
<data name="Action_PreviewPreprocessor" xml:space="preserve">
<value>Vista previa del Preprocesador</value>
</data>
<data name="TeachingTip_WebUiButtonMoved" xml:space="preserve">
<value>El botón &apos;Abrir interfaz de usuario web&apos; se ha movido a la barra de comandos</value>
</data>
<data name="Label_AnotherInstanceAlreadyRunning" xml:space="preserve">
<value>Ya se está ejecutando otra instancia de Stability Matrix. Ciérrelo antes de comenzar uno nuevo.</value>
</data>
<data name="Label_StabilityMatrixAlreadyRunning" xml:space="preserve">
<value>Stability Matrix ya se está ejecutando</value>
</data>
<data name="Label_WorkflowDeletedSuccessfully" xml:space="preserve">
<value>{0} eliminados correctamente</value>
</data>
<data name="Label_WorkflowDeleted" xml:space="preserve">
<value>Workflow Eliminado</value>
</data>
<data name="Label_ErrorRetrievingWorkflows" xml:space="preserve">
<value>Error al recuperar workflows</value>
</data>
<data name="TabLabel_InstalledWorkflows" xml:space="preserve">
<value>Workflows Instalados</value>
</data>
<data name="Label_WorkflowImported" xml:space="preserve">
<value>Workflow Importado</value>
</data>
<data name="Label_FinishedImportingWorkflow" xml:space="preserve">
<value>Terminada la importación del workflow y los nodos personalizados</value>
</data>
<data name="Label_WorkflowImportComplete" xml:space="preserve">
<value>Se han importado el workflow y los nodos personalizados.</value>
</data>
</root>

50
StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx

@ -598,7 +598,7 @@
<value>Informations sur la version de Python</value>
</data>
<data name="Action_Restart" xml:space="preserve">
<value>Redémarrage</value>
<value>Redémarrer</value>
</data>
<data name="Label_ConfirmDelete" xml:space="preserve">
<value>Confirmer la suppression</value>
@ -930,4 +930,52 @@
<data name="Label_RecommendedModelsSubText" xml:space="preserve">
<value>Pendant que votre paquet s&apos;installe, voici quelques modèles que nous recommandons pour vous aidez à démarrer.</value>
</data>
<data name="Label_Notifications" xml:space="preserve">
<value>Notifications</value>
</data>
<data name="Label_NotificationOption_None" xml:space="preserve">
<value>Aucun</value>
</data>
<data name="Label_ComfyRequiredTitle" xml:space="preserve">
<value>ComfyUI est requis</value>
</data>
<data name="Label_ComfyRequiredDetail" xml:space="preserve">
<value>ComfyUI est requis pour installer ce paquet. Voulez vous l&apos;installer maintenant ?</value>
</data>
<data name="Error_PleaseSelectDownloadLocation" xml:space="preserve">
<value>Merci de sélectionner un dossier de téléchargement.</value>
</data>
<data name="Label_SelectDownloadLocation" xml:space="preserve">
<value>Sélectionner un dossier de téléchargement:</value>
</data>
<data name="Label_ToggleAutoScrolling" xml:space="preserve">
<value>Défiler automatiquement à la fin</value>
</data>
<data name="Label_ConfirmExit" xml:space="preserve">
<value>Confirmer la sortie</value>
</data>
<data name="Label_ConfirmExitDetail" xml:space="preserve">
<value>Êtes vous certain de vouloir quitter ? Cela va fermer toutes les instances actuellement lancées.</value>
</data>
<data name="Label_Console" xml:space="preserve">
<value>Console</value>
</data>
<data name="Label_Packages" xml:space="preserve">
<value>Paquets</value>
</data>
<data name="Label_ActionCannotBeUndone" xml:space="preserve">
<value>Cette action ne peut être annulée.</value>
</data>
<data name="Label_AreYouSureDeleteImages" xml:space="preserve">
<value>Êtes vous certain de vouloir supprimer les {0} images?</value>
</data>
<data name="Label_CheckingHardware" xml:space="preserve">
<value>Nous vérifions quelques spécifications matérielles pour vérifier la compatibilité.</value>
</data>
<data name="Label_EverythingLooksGood" xml:space="preserve">
<value>Tout à l&apos;air bon!</value>
</data>
<data name="Label_NvidiaGpuRecommended" xml:space="preserve">
<value>Nous recommandons un GPU avec prise en charge CUDA pour une meilleure expérience. Vous pouvez continuer sans en avoir un, mais certains packages peuvent ne pas fonctionner et l&apos;inférence peut être plus lente.</value>
</data>
</root>

51
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -972,9 +972,30 @@
<data name="Label_SelectDownloadLocation" xml:space="preserve">
<value>Select Download Location:</value>
</data>
<data name="Label_Workflows" xml:space="preserve">
<value>Workflows</value>
</data>
<data name="Label_InfiniteScrolling" xml:space="preserve">
<value>Infinite Scrolling</value>
</data>
<data name="Label_WorkflowBrowser" xml:space="preserve">
<value>Workflow Browser</value>
</data>
<data name="Label_Config" xml:space="preserve">
<value>Config</value>
</data>
<data name="Action_OpenOnOpenArt" xml:space="preserve">
<value>Open on OpenArt</value>
</data>
<data name="Label_NodeDetails" xml:space="preserve">
<value>Node Details</value>
</data>
<data name="Label_WorkflowDescription" xml:space="preserve">
<value>Workflow Description</value>
</data>
<data name="Label_OpenArtBrowser" xml:space="preserve">
<value>OpenArt Browser</value>
</data>
<data name="Action_PreviewPreprocessor" xml:space="preserve">
<value>Preview Preprocessor</value>
</data>
@ -1020,4 +1041,34 @@
<data name="TeachingTip_WebUiButtonMoved" xml:space="preserve">
<value>The 'Open Web UI' button has moved to the command bar</value>
</data>
<data name="Label_AnotherInstanceAlreadyRunning" xml:space="preserve">
<value>Another instance of Stability Matrix is already running. Please close it before starting a new one.</value>
</data>
<data name="Label_StabilityMatrixAlreadyRunning" xml:space="preserve">
<value>Stability Matrix is already running</value>
</data>
<data name="Label_WorkflowDeletedSuccessfully" xml:space="preserve">
<value>{0} deleted successfully</value>
</data>
<data name="Label_WorkflowDeleted" xml:space="preserve">
<value>Workflow Deleted</value>
</data>
<data name="Label_ErrorRetrievingWorkflows" xml:space="preserve">
<value>Error retrieving workflows</value>
</data>
<data name="TabLabel_InstalledWorkflows" xml:space="preserve">
<value>Installed Workflows</value>
</data>
<data name="Label_WorkflowImported" xml:space="preserve">
<value>Workflow Imported</value>
</data>
<data name="Label_FinishedImportingWorkflow" xml:space="preserve">
<value>Finished importing workflow and custom nodes</value>
</data>
<data name="Label_WorkflowImportComplete" xml:space="preserve">
<value>The workflow and custom nodes have been imported.</value>
</data>
<data name="TeachingTip_InferencePromptHelpButton" xml:space="preserve">
<value>Click here to review prompt syntax and how to include Lora / Embeddings.</value>
</data>
</root>

114
StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx

@ -961,4 +961,118 @@
<data name="Label_RecommendedModelsSubText" xml:space="preserve">
<value>Paketiniz yüklenirken başlamanıza yardımcı olması için önerdiğimiz bazı modeller aşağıda verilmiştir.</value>
</data>
<data name="Label_Notifications" xml:space="preserve">
<value>Bildirimler</value>
</data>
<data name="Label_NotificationOption_None" xml:space="preserve">
<value>Hiçbiri</value>
</data>
<data name="Label_ComfyRequiredTitle" xml:space="preserve">
<value>ComfyUI Gerekli</value>
</data>
<data name="Label_ComfyRequiredDetail" xml:space="preserve">
<value>Bu paketi kurmak için ComfyUI gereklidir. Şimdi yüklemek ister misiniz?</value>
</data>
<data name="Error_PleaseSelectDownloadLocation" xml:space="preserve">
<value>Lütfen bir indirme konumu seçin.</value>
</data>
<data name="Label_SelectDownloadLocation" xml:space="preserve">
<value>İndirme Konumunu Seçin:</value>
</data>
<data name="Label_Config" xml:space="preserve">
<value>Yapılandırma</value>
</data>
<data name="Label_ToggleAutoScrolling" xml:space="preserve">
<value>Otomatik Sona Kaydır</value>
</data>
<data name="Label_ConfirmExit" xml:space="preserve">
<value>Çıkışı Onayla</value>
</data>
<data name="Label_ConfirmExitDetail" xml:space="preserve">
<value>Çıkmak istediğine emin misin? Bu aynı zamanda şu anda çalışan tüm paketleri de kapatacaktır.</value>
</data>
<data name="Label_Console" xml:space="preserve">
<value>Konsol</value>
</data>
<data name="Label_WebUi" xml:space="preserve">
<value>Web arayüzü</value>
</data>
<data name="Label_Packages" xml:space="preserve">
<value>Paketler</value>
</data>
<data name="Label_ActionCannotBeUndone" xml:space="preserve">
<value>Bu eylem geri alınamaz.</value>
</data>
<data name="Label_AreYouSureDeleteImages" xml:space="preserve">
<value>{0} görseli silmek istediğinizden emin misiniz?</value>
</data>
<data name="Label_CheckingHardware" xml:space="preserve">
<value>Uyumluluğu belirlemek için bazı donanım özelliklerini kontrol ediyoruz.</value>
</data>
<data name="Label_EverythingLooksGood" xml:space="preserve">
<value>Her şey iyi gözüküyor!</value>
</data>
<data name="Label_NvidiaGpuRecommended" xml:space="preserve">
<value>En iyi deneyim için CUDA destekli bir GPU öneriyoruz. Olmadan da devam edebilirsiniz ancak bazı paketler çalışmayabilir ve çıkarımlar daha yavaş olabilir.</value>
</data>
<data name="Label_Checkpoints" xml:space="preserve">
<value>Kontrol noktaları</value>
</data>
<data name="Label_ModelBrowser" xml:space="preserve">
<value>Model Tarayıcı</value>
</data>
<data name="Label_Workflows" xml:space="preserve">
<value>İş akışları</value>
</data>
<data name="Label_InfiniteScrolling" xml:space="preserve">
<value>Sonsuz Kaydırma</value>
</data>
<data name="Label_WorkflowBrowser" xml:space="preserve">
<value>İş Akışı Tarayıcısı</value>
</data>
<data name="Action_OpenOnOpenArt" xml:space="preserve">
<value>OpenArt&apos;ta aç</value>
</data>
<data name="Label_NodeDetails" xml:space="preserve">
<value>Düğüm Ayrıntıları</value>
</data>
<data name="Label_WorkflowDescription" xml:space="preserve">
<value>İş Akışı Açıklaması</value>
</data>
<data name="Label_OpenArtBrowser" xml:space="preserve">
<value>OpenArt Tarayıcı</value>
</data>
<data name="Action_PreviewPreprocessor" xml:space="preserve">
<value>Ön İşlemciyi Önizle</value>
</data>
<data name="TeachingTip_WebUiButtonMoved" xml:space="preserve">
<value>&apos;Web Kullanıcı Arayüzünü Aç&apos; düğmesi komut çubuğuna taşındı</value>
</data>
<data name="Label_AnotherInstanceAlreadyRunning" xml:space="preserve">
<value>Stability Matrixin başka bir örneği zaten çalışıyor. Yenisini başlatmadan önce lütfen kapatın.</value>
</data>
<data name="Label_StabilityMatrixAlreadyRunning" xml:space="preserve">
<value>Stability Matrix zaten çalışıyor</value>
</data>
<data name="Label_WorkflowDeletedSuccessfully" xml:space="preserve">
<value>{0} başarıyla silindi</value>
</data>
<data name="Label_WorkflowDeleted" xml:space="preserve">
<value>İş Akışı Silindi</value>
</data>
<data name="Label_ErrorRetrievingWorkflows" xml:space="preserve">
<value>İş akışları alınırken hata oluştu</value>
</data>
<data name="TabLabel_InstalledWorkflows" xml:space="preserve">
<value>Yüklü İş Akışları</value>
</data>
<data name="Label_WorkflowImported" xml:space="preserve">
<value>İş Akışı İçeri Aktarıldı</value>
</data>
<data name="Label_FinishedImportingWorkflow" xml:space="preserve">
<value>İş akışının ve özel düğümlerin içe aktarılması tamamlandı</value>
</data>
<data name="Label_WorkflowImportComplete" xml:space="preserve">
<value>İş akışı ve özel düğümler içe aktarıldı.</value>
</data>
</root>

8
StabilityMatrix.Avalonia/Models/IInfinitelyScroll.cs

@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Models;
public interface IInfinitelyScroll
{
Task LoadNextPageAsync();
}

3
StabilityMatrix.Avalonia/Models/ImageSource.cs

@ -58,6 +58,9 @@ public record ImageSource : IDisposable, ITemplateKey<ImageSourceTemplateType>
Bitmap = bitmap;
}
[JsonIgnore]
public Uri? Uri => LocalFile?.FullPath != null ? new Uri(LocalFile.FullPath) : RemoteUrl;
/// <inheritdoc />
public ImageSourceTemplateType TemplateKey { get; private set; }

51
StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs

@ -2,9 +2,11 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Hashing;
using System.Linq;
using System.Text;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
using StabilityMatrix.Core.Models.Inference;
namespace StabilityMatrix.Avalonia.Models.Inference;
@ -17,7 +19,7 @@ public class ModuleApplyStepEventArgs : EventArgs
public NodeDictionary Nodes => Builder.Nodes;
public ModuleApplyStepTemporaryArgs Temp { get; } = new();
public ModuleApplyStepTemporaryArgs Temp { get; set; } = new();
/// <summary>
/// Generation overrides (like hires fix generate, current seed generate, etc.)
@ -26,6 +28,35 @@ public class ModuleApplyStepEventArgs : EventArgs
public List<(string SourcePath, string DestinationRelativePath)> FilesToTransfer { get; init; } = [];
public List<Action<ModuleApplyStepEventArgs>> PreOutputActions { get; init; } = [];
public void InvokeAllPreOutputActions()
{
foreach (var action in PreOutputActions)
{
action(this);
}
}
/// <summary>
/// Creates a new <see cref="ModuleApplyStepEventArgs"/> with the given <see cref="ComfyNodeBuilder"/>.
/// </summary>
/// <returns></returns>
public ModuleApplyStepTemporaryArgs CreateTempFromBuilder()
{
return new ModuleApplyStepTemporaryArgs
{
Primary = Builder.Connections.Primary,
PrimaryVAE = Builder.Connections.PrimaryVAE,
Models = new Dictionary<string, ModelConnections>(
Builder.Connections.Models.ToDictionary(
pair => pair.Key,
pair => new ModelConnections(pair.Value)
)
)
};
}
public void AddFileTransfer(string sourcePath, string destinationRelativePath)
{
FilesToTransfer.Add((sourcePath, destinationRelativePath));
@ -54,22 +85,4 @@ public class ModuleApplyStepEventArgs : EventArgs
return destPath;
}
public class ModuleApplyStepTemporaryArgs
{
/// <summary>
/// Temporary conditioning apply step, used by samplers to apply control net.
/// </summary>
public ConditioningConnections? Conditioning { get; set; }
/// <summary>
/// Temporary refiner conditioning apply step, used by samplers to apply control net.
/// </summary>
public ConditioningConnections? RefinerConditioning { get; set; }
/// <summary>
/// Temporary model apply step, used by samplers to apply control net.
/// </summary>
public ModelNodeConnection? Model { get; set; }
}
}

10
StabilityMatrix.Avalonia/Models/OpenArtCustomNode.cs

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace StabilityMatrix.Avalonia.Models;
public class OpenArtCustomNode
{
public required string Title { get; set; }
public List<string> Children { get; set; } = [];
public bool IsInstalled { get; set; }
}

22
StabilityMatrix.Avalonia/Models/OpenArtMetadata.cs

@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Avalonia.Platform.Storage;
using StabilityMatrix.Core.Models.Api.OpenArt;
namespace StabilityMatrix.Avalonia.Models;
public class OpenArtMetadata
{
[JsonPropertyName("sm_workflow_data")]
public OpenArtSearchResult? Workflow { get; set; }
[JsonIgnore]
public string? FirstThumbnail => Workflow?.Thumbnails?.Select(x => x.Url).FirstOrDefault()?.ToString();
[JsonIgnore]
public List<IStorageFile>? FilePath { get; set; }
[JsonIgnore]
public bool HasMetadata => Workflow?.Creator != null;
}

5
StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs

@ -1,7 +1,10 @@
namespace StabilityMatrix.Avalonia.Models;
using System.Collections.ObjectModel;
namespace StabilityMatrix.Avalonia.Models;
public class PackageOutputCategory
{
public ObservableCollection<PackageOutputCategory> SubDirectories { get; set; } = new();
public required string Name { get; set; }
public required string Path { get; set; }
}

21
StabilityMatrix.Avalonia/Models/PackageSteps/UnpackSiteCustomizeStep.cs

@ -0,0 +1,21 @@
using System;
using System.Threading.Tasks;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
namespace StabilityMatrix.Avalonia.Models.PackageSteps;
public class UnpackSiteCustomizeStep(DirectoryPath venvPath) : IPackageStep
{
public async Task ExecuteAsync(IProgress<ProgressReport>? progress = null)
{
var sitePackages = venvPath.JoinDir(PyVenvRunner.RelativeSitePackagesPath);
var file = sitePackages.JoinFile("sitecustomize.py");
file.Directory?.Create();
await Assets.PyScriptSiteCustomize.ExtractTo(file);
}
public string ProgressTitle => "Unpacking prerequisites...";
}

1
StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs

@ -44,6 +44,7 @@ public interface IInferenceClientManager : IDisposable, INotifyPropertyChanged,
IObservableCollection<ComfySampler> Samplers { get; }
IObservableCollection<ComfyUpscaler> Upscalers { get; }
IObservableCollection<ComfyScheduler> Schedulers { get; }
IObservableCollection<ComfyAuxPreprocessor> Preprocessors { get; }
Task CopyImageToInputAsync(FilePath imageFile, CancellationToken cancellationToken = default);

8
StabilityMatrix.Avalonia/Services/IModelDownloadLinkHandler.cs

@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Services;
public interface IModelDownloadLinkHandler
{
Task StartListening();
}

3
StabilityMatrix.Avalonia/Services/INavigationService.cs

@ -37,4 +37,7 @@ public interface INavigationService<[SuppressMessage("ReSharper", "UnusedTypePar
void NavigateTo(ViewModelBase viewModel, NavigationTransitionInfo? transitionInfo = null);
bool GoBack();
bool CanGoBack { get; }
object? CurrentPageDataContext { get; }
}

20
StabilityMatrix.Avalonia/Services/InferenceClientManager.cs

@ -14,6 +14,7 @@ using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Inference;
using StabilityMatrix.Core.Models;
@ -101,6 +102,11 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
public IObservableCollection<ComfyScheduler> Schedulers { get; } =
new ObservableCollectionExtended<ComfyScheduler>();
public IObservableCollection<ComfyAuxPreprocessor> Preprocessors { get; } =
new ObservableCollectionExtended<ComfyAuxPreprocessor>();
private readonly SourceCache<ComfyAuxPreprocessor, string> preprocessorsSource = new(p => p.Value);
public InferenceClientManager(
ILogger<InferenceClientManager> logger,
IApiFactory apiFactory,
@ -166,6 +172,8 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
schedulersSource.Connect().DeferUntilLoaded().Bind(Schedulers).Subscribe();
preprocessorsSource.Connect().DeferUntilLoaded().Bind(Preprocessors).Subscribe();
settingsManager.RegisterOnLibraryDirSet(_ =>
{
Dispatcher.UIThread.Post(ResetSharedProperties, DispatcherPriority.Background);
@ -270,6 +278,15 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
});
logger.LogTrace("Loaded scheduler methods: {@Schedulers}", schedulerNames);
}
// Add preprocessor names from Inference_Core_AIO_Preprocessor node (might not exist if no extension)
if (
await Client.GetOptionalNodeOptionNamesAsync("Inference_Core_AIO_Preprocessor", "preprocessor") is
{ } preprocessorNames
)
{
preprocessorsSource.EditDiff(preprocessorNames.Select(n => new ComfyAuxPreprocessor(n)));
}
}
/// <summary>
@ -342,6 +359,9 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
u => !modelUpscalersSource.Lookup(u.Name).HasValue
);
downloadableUpscalersSource.EditDiff(remoteUpscalers, ComfyUpscaler.Comparer);
// Default Preprocessors
preprocessorsSource.EditDiff(ComfyAuxPreprocessor.Defaults);
}
/// <inheritdoc />

251
StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs

@ -0,0 +1,251 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using MessagePipe;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.Services;
[Singleton(typeof(IModelDownloadLinkHandler)), Singleton(typeof(IAsyncDisposable))]
public class ModelDownloadLinkHandler(
IDistributedSubscriber<string, Uri> uriHandlerSubscriber,
ILogger<ModelDownloadLinkHandler> logger,
ICivitApi civitApi,
INotificationService notificationService,
ISettingsManager settingsManager,
IDownloadService downloadService,
ITrackedDownloadService trackedDownloadService
) : IAsyncDisposable, IModelDownloadLinkHandler
{
private IAsyncDisposable? uriHandlerSubscription;
private const string DownloadCivitModel = "downloadCivitModel";
public async Task StartListening()
{
uriHandlerSubscription = await uriHandlerSubscriber.SubscribeAsync(
UriHandler.IpcKeySend,
UriReceivedHandler
);
}
public async ValueTask DisposeAsync()
{
if (uriHandlerSubscription is not null)
{
await uriHandlerSubscription.DisposeAsync();
uriHandlerSubscription = null;
}
}
private void UriReceivedHandler(Uri receivedUri)
{
logger.LogDebug("ModelDownloadLinkHandler Received URI: {Uri}", receivedUri.PathAndQuery);
if (!receivedUri.Host.Equals(DownloadCivitModel, StringComparison.OrdinalIgnoreCase))
return;
var queryDict = HttpUtility.ParseQueryString(receivedUri.Query);
var modelIdStr = queryDict["modelId"];
var modelVersionIdStr = queryDict["modelVersionId"];
var type = queryDict["type"];
var format = queryDict["format"];
var size = queryDict["size"];
var fp = queryDict["fp"];
if (
string.IsNullOrWhiteSpace(modelIdStr)
|| string.IsNullOrWhiteSpace(type)
|| string.IsNullOrWhiteSpace(format)
|| !int.TryParse(modelIdStr, out var modelId)
|| !Enum.TryParse<CivitFileType>(type, out var civitFileType)
|| !Enum.TryParse<CivitModelFormat>(format, out var civitFormat)
)
{
logger.LogError("ModelDownloadLinkHandler: Invalid query parameters");
Dispatcher.UIThread.Post(
() =>
notificationService.Show(
new Notification(
"Invalid Download Link",
"The download link is invalid",
NotificationType.Error
)
)
);
return;
}
Dispatcher.UIThread.Post(
() =>
notificationService.Show(
"Link Received",
"Successfully received download link",
NotificationType.Warning
)
);
var modelTask = civitApi.GetModelById(modelId);
modelTask.Wait();
var model = modelTask.Result;
var useModelVersion = !string.IsNullOrWhiteSpace(modelVersionIdStr);
var modelVersionId = useModelVersion ? int.Parse(modelVersionIdStr) : 0;
var modelVersion = useModelVersion
? model.ModelVersions?.FirstOrDefault(x => x.Id == modelVersionId)
: model.ModelVersions?.FirstOrDefault();
if (modelVersion is null)
{
logger.LogError("ModelDownloadLinkHandler: Model version not found");
Dispatcher.UIThread.Post(
() =>
notificationService.Show(
new Notification(
"Model has no versions available",
"This model has no versions available for download",
NotificationType.Error
)
)
);
return;
}
var possibleFiles = modelVersion.Files?.Where(
x => x.Type == civitFileType && x.Metadata.Format == civitFormat
);
if (!string.IsNullOrWhiteSpace(fp) && Enum.TryParse<CivitModelFpType>(fp, out var fpType))
{
possibleFiles = possibleFiles?.Where(x => x.Metadata.Fp == fpType);
}
if (!string.IsNullOrWhiteSpace(size) && Enum.TryParse<CivitModelSize>(size, out var modelSize))
{
possibleFiles = possibleFiles?.Where(x => x.Metadata.Size == modelSize);
}
possibleFiles = possibleFiles?.ToList();
if (possibleFiles is null)
{
Dispatcher.UIThread.Post(
() =>
notificationService.Show(
new Notification(
"Model has no files available",
"This model has no files available for download",
NotificationType.Error
)
)
);
logger.LogError("ModelDownloadLinkHandler: Model file not found");
return;
}
var selectedFile = possibleFiles.FirstOrDefault() ?? modelVersion.Files?.FirstOrDefault();
var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory);
var downloadDirectory = rootModelsDirectory.JoinDir(
selectedFile.Type == CivitFileType.VAE
? SharedFolderType.VAE.GetStringValue()
: model.Type.ConvertTo<SharedFolderType>().GetStringValue()
);
downloadDirectory.Create();
var downloadPath = downloadDirectory.JoinFile(selectedFile.Name);
// Create tracked download
var download = trackedDownloadService.NewDownload(selectedFile.DownloadUrl, downloadPath);
// Download model info and preview first
var saveCmInfoTask = SaveCmInfo(model, modelVersion, selectedFile, downloadDirectory);
var savePreviewImageTask = SavePreviewImage(modelVersion, downloadPath);
Task.WaitAll([saveCmInfoTask, savePreviewImageTask]);
var cmInfoPath = saveCmInfoTask.Result;
var previewImagePath = savePreviewImageTask.Result;
// Add hash info
download.ExpectedHashSha256 = selectedFile.Hashes.SHA256;
// Add files to cleanup list
download.ExtraCleanupFileNames.Add(cmInfoPath);
if (previewImagePath is not null)
{
download.ExtraCleanupFileNames.Add(previewImagePath);
}
// Add hash context action
download.ContextAction = CivitPostDownloadContextAction.FromCivitFile(selectedFile);
download.Start();
Dispatcher.UIThread.Post(
() => notificationService.Show("Download Started", $"Downloading {selectedFile.Name}")
);
}
private static async Task<FilePath> SaveCmInfo(
CivitModel model,
CivitModelVersion modelVersion,
CivitFile modelFile,
DirectoryPath downloadDirectory
)
{
var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Name);
var modelInfo = new ConnectedModelInfo(model, modelVersion, modelFile, DateTime.UtcNow);
await modelInfo.SaveJsonToDirectory(downloadDirectory, modelFileName);
var jsonName = $"{modelFileName}.cm-info.json";
return downloadDirectory.JoinFile(jsonName);
}
/// <summary>
/// Saves the preview image to the same directory as the model file
/// </summary>
/// <param name="modelVersion"></param>
/// <param name="modelFilePath"></param>
/// <returns>The file path of the saved preview image</returns>
private async Task<FilePath?> SavePreviewImage(CivitModelVersion modelVersion, FilePath modelFilePath)
{
// Skip if model has no images
if (modelVersion.Images == null || modelVersion.Images.Count == 0)
{
return null;
}
var image = modelVersion.Images.FirstOrDefault(x => x.Type == "image");
if (image is null)
return null;
var imageExtension = Path.GetExtension(image.Url).TrimStart('.');
if (imageExtension is "jpg" or "jpeg" or "png")
{
var imageDownloadPath = modelFilePath.Directory!.JoinFile(
$"{modelFilePath.NameWithoutExtension}.preview.{imageExtension}"
);
var imageTask = downloadService.DownloadToFileAsync(image.Url, imageDownloadPath);
await notificationService.TryAsync(imageTask, "Could not download preview image");
return imageDownloadPath;
}
return null;
}
}

5
StabilityMatrix.Avalonia/Services/NavigationService.cs

@ -1,5 +1,6 @@
using System;
using System.Linq;
using Avalonia.Controls;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media.Animation;
using FluentAvalonia.UI.Navigation;
@ -188,4 +189,8 @@ public class NavigationService<T> : INavigationService<T>
_frame.GoBack();
return true;
}
public bool CanGoBack => _frame?.CanGoBack ?? false;
public object? CurrentPageDataContext => (_frame?.Content as Control)?.DataContext;
}

153
StabilityMatrix.Avalonia/Services/RunningPackageService.cs

@ -0,0 +1,153 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.Services;
[Singleton]
public partial class RunningPackageService(
ILogger<RunningPackageService> logger,
IPackageFactory packageFactory,
INotificationService notificationService,
ISettingsManager settingsManager,
IPyRunner pyRunner
) : ObservableObject
{
// 🤔 what if we put the ConsoleViewModel inside the BasePackage? 🤔
[ObservableProperty]
private ObservableDictionary<Guid, RunningPackageViewModel> runningPackages = [];
public async Task<PackagePair?> StartPackage(InstalledPackage installedPackage, string? command = null)
{
var activeInstallName = installedPackage.PackageName;
var basePackage = string.IsNullOrWhiteSpace(activeInstallName)
? null
: packageFactory.GetNewBasePackage(installedPackage);
if (basePackage == null)
{
logger.LogWarning(
"During launch, package name '{PackageName}' did not match a definition",
activeInstallName
);
notificationService.Show(
new Notification(
"Package name invalid",
"Install package name did not match a definition. Please reinstall and let us know about this issue.",
NotificationType.Error
)
);
return null;
}
// If this is the first launch (LaunchArgs is null),
// load and save a launch options dialog vm
// so that dynamic initial values are saved.
if (installedPackage.LaunchArgs == null)
{
var definitions = basePackage.LaunchOptions;
// Create config cards and save them
var cards = LaunchOptionCard
.FromDefinitions(definitions, Array.Empty<LaunchOption>())
.ToImmutableArray();
var args = cards.SelectMany(c => c.Options).ToList();
logger.LogDebug(
"Setting initial launch args: {Args}",
string.Join(", ", args.Select(o => o.ToArgString()?.ToRepr()))
);
settingsManager.SaveLaunchArgs(installedPackage.Id, args);
}
if (basePackage is not StableSwarm)
{
await pyRunner.Initialize();
}
// Get path from package
var packagePath = new DirectoryPath(settingsManager.LibraryDir, installedPackage.LibraryPath!);
if (basePackage is not StableSwarm)
{
// Unpack sitecustomize.py to venv
await UnpackSiteCustomize(packagePath.JoinDir("venv"));
}
// Clear console and start update processing
var console = new ConsoleViewModel();
console.StartUpdates();
// Update shared folder links (in case library paths changed)
await basePackage.UpdateModelFolders(
packagePath,
installedPackage.PreferredSharedFolderMethod ?? basePackage.RecommendedSharedFolderMethod
);
if (installedPackage.UseSharedOutputFolder)
{
await basePackage.SetupOutputFolderLinks(installedPackage.FullPath!);
}
// Load user launch args from settings and convert to string
var userArgs = installedPackage.LaunchArgs ?? [];
var userArgsString = string.Join(" ", userArgs.Select(opt => opt.ToArgString()));
// Join with extras, if any
userArgsString = string.Join(" ", userArgsString, basePackage.ExtraLaunchArguments);
// Use input command if provided, otherwise use package launch command
command ??= basePackage.LaunchCommand;
await basePackage.RunPackage(packagePath, command, userArgsString, o => console.Post(o));
var runningPackage = new PackagePair(installedPackage, basePackage);
var viewModel = new RunningPackageViewModel(
settingsManager,
notificationService,
this,
runningPackage,
console
);
RunningPackages.Add(runningPackage.InstalledPackage.Id, viewModel);
return runningPackage;
}
public async Task StopPackage(Guid id)
{
if (RunningPackages.TryGetValue(id, out var vm))
{
var runningPackage = vm.RunningPackage;
await runningPackage.BasePackage.WaitForShutdown();
RunningPackages.Remove(id);
}
}
public RunningPackageViewModel? GetRunningPackageViewModel(Guid id) =>
RunningPackages.TryGetValue(id, out var vm) ? vm : null;
private static async Task UnpackSiteCustomize(DirectoryPath venvPath)
{
var sitePackages = venvPath.JoinDir(PyVenvRunner.RelativeSitePackagesPath);
var file = sitePackages.JoinFile("sitecustomize.py");
file.Directory?.Create();
await Assets.PyScriptSiteCustomize.ExtractTo(file, true);
}
}

14
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -39,13 +39,14 @@
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.2.1" />
<PackageReference Include="AutoComplete.Net" Version="1.2211.2014.42"/>
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.6" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.9" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.0.0.2" />
<PackageReference Include="Avalonia" Version="11.0.9" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.9" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.9" />
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.9" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.HtmlRenderer" Version="11.0.0" />
<PackageReference Include="Avalonia.Labs.Controls" Version="11.0.3" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.6" />
@ -197,6 +198,9 @@
<DependentUpon>NewInstallerDialog.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Models\OpenArtMetadata.cs">
<Generator>MSBuild:GenerateCodeFromAttributes</Generator>
</Compile>
</ItemGroup>
<!-- set HUSKY to 0 to disable, or opt-in during CI by setting HUSKY to 1 -->

3
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings

@ -2,4 +2,5 @@
<s:String x:Key="/Default/CodeEditing/Localization/Localizable/@EntryValue">Yes</s:String>
<s:String x:Key="/Default/CodeEditing/Localization/LocalizableInspector/@EntryValue">Pessimistic</s:String>
<s:String x:Key="/Default/CodeInspection/Daemon/ConfigureAwaitAnalysisMode/@EntryValue">UI</s:String>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls_005Cpropertygrid/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls_005Cpropertygrid/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls_005Cvendorlabs_005Casyncimage/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

73
StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml

@ -99,6 +99,43 @@
</Style>
</Style>
<!-- Danger -->
<Style Selector="Button.borderless-danger">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ThemeRedColor}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ThemeDarkRedColor}"/>
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ThemeDarkDarkRedColor}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Info -->
<Style Selector="Button.info">
<Style Selector="^ /template/ ui|FABorder#Root">
@ -140,6 +177,42 @@
</Style>
</Style>
<Style Selector="Button.borderless-info">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ThemeLightBlueColor}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ThemeBlueColor}"/>
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ThemeDarkBlueColor}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!--Accent Button-->
<Style Selector="Button.accent">
<Style Selector="^ /template/ ui|FABorder#Root">

403
StabilityMatrix.Avalonia/Styles/CommandBarButtonStyles.axaml

@ -0,0 +1,403 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
<Design.PreviewWith>
<Border Padding="20">
<ui:CommandBar
Margin="8"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
HorizontalAlignment="Left"
HorizontalContentAlignment="Left"
DefaultLabelPosition="Right">
<ui:CommandBar.PrimaryCommands>
<ui:CommandBarButton Classes="success" Label="Success Button" Margin="8"
HorizontalAlignment="Center" />
<ui:CommandBarButton Classes="accent" Label="FA Accent Button" Margin="8"
HorizontalAlignment="Center" />
<ui:CommandBarButton Classes="systemaccent" Label="System Accent Button" Margin="8"
HorizontalAlignment="Center" />
<ui:CommandBarButton Classes="danger" Label="Danger Button" Margin="8" HorizontalAlignment="Center" />
<ui:CommandBarButton Classes="info" Label="Info Button" Margin="8" HorizontalAlignment="Center" />
<ui:CommandBarButton Classes="transparent-info" Label="Semi-Transparent Info Button" Margin="8"
HorizontalAlignment="Center" />
<ui:CommandBarButton Classes="transparent" Label="Transparent Button" Margin="8"
HorizontalAlignment="Center" />
<ui:CommandBarButton Classes="transparent-full" Label="Transparent Button" Margin="8"
HorizontalAlignment="Center" />
<ui:CommandBarButton Label="Disabled Button" Margin="8" IsEnabled="False"
HorizontalAlignment="Center" />
</ui:CommandBar.PrimaryCommands>
</ui:CommandBar>
</Border>
</Design.PreviewWith>
<!-- Success -->
<Style Selector="ui|CommandBarButton.success">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeGreenColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeGreenColor}" />
</Style>
<Style Selector="^ /template/ TextBlock#TextLabel">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeDarkGreenColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkGreenColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="Green" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeDarkDarkGreenColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkDarkGreenColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Danger -->
<Style Selector="ui|CommandBarButton.danger">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeRedColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeRedColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeDarkRedColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkRedColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeDarkDarkRedColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkDarkRedColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Info -->
<Style Selector="ui|CommandBarButton.info">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeLightBlueColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeLightBlueColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeBlueColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBlueColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeDarkBlueColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkBlueColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!--Accent Button-->
<Style Selector="ui|CommandBarButton.accent">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrush}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPointerOver}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPressed}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- SystemAccent -->
<Style Selector="ui|CommandBarButton.systemaccent">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource SystemAccentColorDark1}"/>
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColorDark1}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource SystemAccentColorDark2}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColorDark2}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Transparent -->
<Style Selector="ui|CommandBarButton.transparent">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundPressed}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Semi-Transparent Info -->
<Style Selector="ui|CommandBarButton.transparent-info">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeLightBlueColorTransparent}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeLightBlueColorTransparent}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeBlueColorTransparent}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBlueColorTransparent}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeDarkBlueColorTransparent}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkBlueColorTransparent}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Transparent red -->
<Style Selector="ui|CommandBarButton.transparent-red">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeCoralRedColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeCoralRedColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ThemeDarkCoralRedColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkCoralRedColor}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Full Transparent -->
<Style Selector="ui|CommandBarButton.transparent-full">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlTransparentBrush}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundDisabled}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundPressed}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder">
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlTransparentBrush}" />
</Style>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
</Styles>

41
StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml

@ -6,7 +6,10 @@
xmlns:mocks="using:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters">
<Design.PreviewWith>
<Panel Width="450" Height="600">
@ -27,7 +30,9 @@
</StackPanel>
</Panel>
</Design.PreviewWith>
<converters:FileUriConverter x:Key="FileUriConverter" />
<!-- ReSharper disable once Xaml.StaticResourceNotResolved -->
<ControlTheme
x:Key="BetterComboBoxItemHybridModelTheme"
@ -42,15 +47,14 @@
ColumnSpacing="6"
RowSpacing="0">
<!-- Image -->
<controls:BetterAdvancedImage
<vendorLabs:BetterAsyncImage
Width="64"
Height="96"
CornerRadius="6"
IsVisible="{Binding Local.PreviewImageFullPathGlobal, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=''}"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal, FallbackValue=''}"
Stretch="UniformToFill"
StretchDirection="Both" />
Source="{Binding Local.PreviewImageFullPathGlobal, FallbackValue='', Converter={StaticResource FileUriConverter}}"
Stretch="UniformToFill"/>
<StackPanel
Grid.Column="1"
MaxWidth="300"
@ -125,18 +129,14 @@
ColumnSpacing="8"
TextBlock.TextTrimming="CharacterEllipsis"
TextBlock.TextWrapping="NoWrap">
<controls:BetterAdvancedImage
<vendorLabs:BetterAsyncImage
Grid.RowSpan="2"
Width="42"
Height="42"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal}"
Stretch="UniformToFill"
StretchDirection="Both">
<controls:BetterAdvancedImage.Clip>
<EllipseGeometry Rect="0,0,42,42" />
</controls:BetterAdvancedImage.Clip>
</controls:BetterAdvancedImage>
CornerRadius="40"
Source="{Binding Local.PreviewImageFullPathGlobal, Converter={StaticResource FileUriConverter}}"
Stretch="UniformToFill"/>
<!-- Text -->
<sg:SpacedGrid
@ -217,18 +217,15 @@
ColumnSpacing="8"
TextBlock.TextTrimming="CharacterEllipsis"
TextBlock.TextWrapping="NoWrap">
<controls:BetterAdvancedImage
<vendorLabs:BetterAsyncImage
Grid.RowSpan="2"
Width="36"
Height="36"
CornerRadius="60"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal}"
Stretch="UniformToFill"
StretchDirection="Both">
<controls:BetterAdvancedImage.Clip>
<EllipseGeometry Rect="0,0,36,36" />
</controls:BetterAdvancedImage.Clip>
</controls:BetterAdvancedImage>
Source="{Binding Local.PreviewImageFullPathGlobal, Converter={StaticResource FileUriConverter}}"
Stretch="UniformToFill">
</vendorLabs:BetterAsyncImage>
<!-- Text -->
<sg:SpacedGrid

1
StabilityMatrix.Avalonia/Styles/ThemeColors.axaml

@ -21,6 +21,7 @@
<Color x:Key="ThemeCyanColor">#00BCD4</Color>
<Color x:Key="ThemeTealColor">#009688</Color>
<Color x:Key="ThemeDarkDarkGreenColor">#2C582C</Color>
<SolidColorBrush x:Key="ThemeDarkDarkGreenColorBrush">#2C582C</SolidColorBrush>
<Color x:Key="ThemeDarkGreenColor">#3A783C</Color>
<Color x:Key="ThemeGreenColor">#4BA04F</Color>
<Color x:Key="ThemeGreenColorTransparent">#AA4BA04F</Color>

119
StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs

@ -16,10 +16,9 @@ using Avalonia.Threading;
using CommunityToolkit.Mvvm.Input;
using ExifLibrary;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.DependencyInjection;
using Nito.Disposables.Internals;
using NLog;
using Refit;
using Semver;
using SkiaSharp;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Helpers;
@ -59,6 +58,7 @@ public abstract partial class InferenceGenerationViewModelBase
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ISettingsManager settingsManager;
private readonly RunningPackageService runningPackageService;
private readonly INotificationService notificationService;
private readonly ServiceManager<ViewModelBase> vmFactory;
@ -79,12 +79,14 @@ public abstract partial class InferenceGenerationViewModelBase
ServiceManager<ViewModelBase> vmFactory,
IInferenceClientManager inferenceClientManager,
INotificationService notificationService,
ISettingsManager settingsManager
ISettingsManager settingsManager,
RunningPackageService runningPackageService
)
: base(notificationService)
{
this.notificationService = notificationService;
this.settingsManager = settingsManager;
this.runningPackageService = runningPackageService;
this.vmFactory = vmFactory;
ClientManager = inferenceClientManager;
@ -259,6 +261,30 @@ public abstract partial class InferenceGenerationViewModelBase
}
}
public async Task RunCustomGeneration(
InferenceQueueCustomPromptEventArgs args,
CancellationToken cancellationToken = default
)
{
if (ClientManager.Client is not { } client)
{
throw new InvalidOperationException("Client is not connected");
}
var generationArgs = new ImageGenerationEventArgs
{
Client = client,
Nodes = args.Builder.ToNodeDictionary(),
OutputNodeNames = args.Builder.Connections.OutputNodeNames.ToArray(),
Project = InferenceProjectDocument.FromLoadable(this),
FilesToTransfer = args.FilesToTransfer,
Parameters = new GenerationParameters(),
ClearOutputImages = true
};
await RunGeneration(generationArgs, cancellationToken);
}
/// <summary>
/// Runs a generation task
/// </summary>
@ -643,12 +669,10 @@ public abstract partial class InferenceGenerationViewModelBase
{
// Get prompt required extensions
// Just static for now but could do manifest lookup when we support custom workflows
var requiredExtensions = nodeDictionary
.ClassTypeRequiredExtensions.Values.SelectMany(x => x)
.ToHashSet();
var requiredExtensionSpecifiers = nodeDictionary.RequiredExtensions.ToList();
// Skip if no extensions required
if (requiredExtensions.Count == 0)
if (requiredExtensionSpecifiers.Count == 0)
{
return true;
}
@ -661,20 +685,63 @@ public abstract partial class InferenceGenerationViewModelBase
await ((GitPackageExtensionManager)manager).GetInstalledExtensionsLiteAsync(
localPackagePair.InstalledPackage
)
).ToImmutableArray();
).ToList();
var localExtensionsByGitUrl = localExtensions
.Where(ext => ext.GitRepositoryUrl is not null)
.ToDictionary(ext => ext.GitRepositoryUrl!, ext => ext);
var requiredExtensionReferences = requiredExtensionSpecifiers
.Select(specifier => specifier.Name)
.ToHashSet();
var missingExtensions = requiredExtensions
.Except(localExtensions.Select(ext => ext.GitRepositoryUrl).WhereNotNull())
.ToImmutableArray();
var missingExtensions = new List<ExtensionSpecifier>();
var outOfDateExtensions =
new List<(ExtensionSpecifier Specifier, InstalledPackageExtension Installed)>();
if (missingExtensions.Length == 0)
// Check missing extensions and out of date extensions
foreach (var specifier in requiredExtensionSpecifiers)
{
if (!localExtensionsByGitUrl.TryGetValue(specifier.Name, out var localExtension))
{
missingExtensions.Add(specifier);
continue;
}
// Check if constraint is specified
if (specifier.Constraint is not null && specifier.TryGetSemVersionRange(out var semVersionRange))
{
// Get version to compare
localExtension = await manager.GetInstalledExtensionInfoAsync(localExtension);
// Try to parse local tag to semver
if (
localExtension.Version?.Tag is not null
&& SemVersion.TryParse(
localExtension.Version.Tag,
SemVersionStyles.AllowV,
out var localSemVersion
)
)
{
// Check if not satisfied
if (!semVersionRange.Contains(localSemVersion))
{
outOfDateExtensions.Add((specifier, localExtension));
}
}
}
}
if (missingExtensions.Count == 0 && outOfDateExtensions.Count == 0)
{
return true;
}
var dialog = DialogHelper.CreateMarkdownDialog(
$"#### The following extensions are required for this workflow:\n"
+ $"{string.Join("\n- ", missingExtensions)}",
+ $"{string.Join("\n- ", missingExtensions.Select(ext => ext.Name))}"
+ $"{string.Join("\n- ", outOfDateExtensions.Select(pair => $"{pair.Item1.Name} {pair.Specifier.Constraint} {pair.Specifier.Version} (Current Version: {pair.Installed.Version?.Tag})"))}",
"Install Required Extensions?"
);
@ -692,13 +759,14 @@ public abstract partial class InferenceGenerationViewModelBase
var steps = new List<IPackageStep>();
foreach (var missingExtensionUrl in missingExtensions)
// Add install for missing extensions
foreach (var missingExtension in missingExtensions)
{
if (!manifestExtensionsMap.TryGetValue(missingExtensionUrl, out var extension))
if (!manifestExtensionsMap.TryGetValue(missingExtension.Name, out var extension))
{
Logger.Warn(
"Extension {MissingExtensionUrl} not found in manifests",
missingExtensionUrl
missingExtension.Name
);
continue;
}
@ -706,6 +774,18 @@ public abstract partial class InferenceGenerationViewModelBase
steps.Add(new InstallExtensionStep(manager, localPackagePair.InstalledPackage, extension));
}
// Add update for out of date extensions
foreach (var (specifier, installed) in outOfDateExtensions)
{
if (!manifestExtensionsMap.TryGetValue(specifier.Name, out var extension))
{
Logger.Warn("Extension {MissingExtensionUrl} not found in manifests", specifier.Name);
continue;
}
steps.Add(new UpdateExtensionStep(manager, localPackagePair.InstalledPackage, installed));
}
var runner = new PackageModificationRunner
{
ShowDialogOnStart = true,
@ -722,15 +802,12 @@ public abstract partial class InferenceGenerationViewModelBase
return;
// Restart Package
// TODO: This should be handled by some DI package manager service
var launchPage = App.Services.GetRequiredService<LaunchPageViewModel>();
try
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
await launchPage.Stop();
await launchPage.LaunchAsync();
await runningPackageService.StopPackage(localPackagePair.InstalledPackage.Id);
await runningPackageService.StartPackage(localPackagePair.InstalledPackage);
});
}
catch (Exception e)

21
StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs

@ -18,6 +18,7 @@ using FluentAvalonia.UI.Media.Animation;
using Microsoft.Extensions.DependencyInjection;
using NLog;
using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
@ -98,11 +99,7 @@ public abstract partial class InferenceTabViewModelBase
protected async Task<ViewState> SaveViewState()
{
var eventArgs = new SaveViewStateEventArgs();
saveViewStateRequestedEventManager?.RaiseEvent(
this,
eventArgs,
nameof(SaveViewStateRequested)
);
saveViewStateRequestedEventManager?.RaiseEvent(this, eventArgs, nameof(SaveViewStateRequested));
if (eventArgs.StateTask is not { } stateTask)
{
@ -128,7 +125,7 @@ public abstract partial class InferenceTabViewModelBase
// TODO: Dock reset not working, using this hack for now to get a new view
var navService = App.Services.GetRequiredService<INavigationService<MainWindowViewModel>>();
navService.NavigateTo<LaunchPageViewModel>(new SuppressNavigationTransitionInfo());
navService.NavigateTo<NewPackageManagerViewModel>(new SuppressNavigationTransitionInfo());
((IPersistentViewProvider)this).AttachedPersistentView = null;
navService.NavigateTo<InferenceViewModel>(new BetterEntranceNavigationTransition());
}
@ -157,9 +154,7 @@ public abstract partial class InferenceTabViewModelBase
if (result == ContentDialogResult.Primary && textFields[0].Text is { } json)
{
LoadViewState(
new LoadViewStateEventArgs { State = new ViewState { DockLayout = json } }
);
LoadViewState(new LoadViewStateEventArgs { State = new ViewState { DockLayout = json } });
}
}
@ -226,9 +221,7 @@ public abstract partial class InferenceTabViewModelBase
if (this is IParametersLoadableState paramsLoadableVm)
{
Dispatcher.UIThread.Invoke(
() => paramsLoadableVm.LoadStateFromParameters(parameters)
);
Dispatcher.UIThread.Invoke(() => paramsLoadableVm.LoadStateFromParameters(parameters));
}
else
{
@ -257,7 +250,7 @@ public abstract partial class InferenceTabViewModelBase
// 1. Context drop for LocalImageFile
if (e.Data.GetDataFormats().Contains("Context"))
{
if (e.Data.Get("Context") is LocalImageFile imageFile)
if (e.Data.GetContext<LocalImageFile>() is { } imageFile)
{
e.Handled = true;
return;
@ -282,7 +275,7 @@ public abstract partial class InferenceTabViewModelBase
// 1. Context drop for LocalImageFile
if (e.Data.GetDataFormats().Contains("Context"))
{
if (e.Data.Get("Context") is LocalImageFile imageFile)
if (e.Data.GetContext<LocalImageFile>() is { } imageFile)
{
e.Handled = true;

3
StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs

@ -11,6 +11,7 @@ using NLog;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
using StabilityMatrix.Core.Models.Inference;
namespace StabilityMatrix.Avalonia.ViewModels.Base;
@ -20,12 +21,14 @@ namespace StabilityMatrix.Avalonia.ViewModels.Base;
[JsonDerivedType(typeof(UpscalerCardViewModel), UpscalerCardViewModel.ModuleKey)]
[JsonDerivedType(typeof(ControlNetCardViewModel), ControlNetCardViewModel.ModuleKey)]
[JsonDerivedType(typeof(PromptExpansionCardViewModel), PromptExpansionCardViewModel.ModuleKey)]
[JsonDerivedType(typeof(LayerDiffuseCardViewModel), LayerDiffuseCardViewModel.ModuleKey)]
[JsonDerivedType(typeof(FreeUModule))]
[JsonDerivedType(typeof(HiresFixModule))]
[JsonDerivedType(typeof(UpscalerModule))]
[JsonDerivedType(typeof(ControlNetModule))]
[JsonDerivedType(typeof(SaveImageModule))]
[JsonDerivedType(typeof(PromptExpansionModule))]
[JsonDerivedType(typeof(LayerDiffuseModule))]
public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

34
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
@ -19,6 +20,7 @@ using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Database;
@ -157,8 +159,10 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
// Try to find a valid image
var image = images
?.Where(img => LocalModelFile.SupportedImageExtensions.Any(img.Url.Contains))
.FirstOrDefault(image => nsfwEnabled || image.Nsfw == "None");
?.Where(
img => LocalModelFile.SupportedImageExtensions.Any(img.Url.Contains) && img.Type == "image"
)
.FirstOrDefault(image => nsfwEnabled || image.NsfwLevel <= 1);
if (image != null)
{
CardImage = new Uri(image.Url);
@ -216,7 +220,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
MaxDialogHeight = 950,
};
var prunedDescription = PruneDescription(model);
var prunedDescription = Utilities.RemoveHtml(model.Description);
var viewModel = dialogFactory.Get<SelectModelVersionViewModel>();
viewModel.Dialog = dialog;
@ -255,7 +259,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
{
var subFolder =
viewModel?.SelectedInstallLocation
?? Path.Combine("Models", model.Type.ConvertTo<SharedFolderType>().GetStringValue());
?? Path.Combine(@"Models", model.Type.ConvertTo<SharedFolderType>().GetStringValue());
downloadPath = Path.Combine(settingsManager.LibraryDir, subFolder);
}
@ -263,23 +267,6 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
await DoImport(model, downloadPath, selectedVersion, selectedFile);
}
private static string PruneDescription(CivitModel model)
{
var prunedDescription =
model
.Description?.Replace("<br/>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("<br />", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</p>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h1>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h2>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h3>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h4>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h5>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h6>", $"{Environment.NewLine}{Environment.NewLine}") ?? string.Empty;
prunedDescription = HtmlRegex().Replace(prunedDescription, string.Empty);
return prunedDescription;
}
private static async Task<FilePath> SaveCmInfo(
CivitModel model,
CivitModelVersion modelVersion,
@ -310,7 +297,10 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
return null;
}
var image = modelVersion.Images[0];
var image = modelVersion.Images.FirstOrDefault(x => x.Type == "image");
if (image is null)
return null;
var imageExtension = Path.GetExtension(image.Url).TrimStart('.');
if (imageExtension is "jpg" or "jpeg" or "png")
{

220
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs

@ -1,13 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Collections;
@ -15,12 +12,15 @@ using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Alias;
using DynamicData.Binding;
using LiteDB;
using LiteDB.Async;
using NLog;
using OneOf.Types;
using Refit;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
@ -41,24 +41,24 @@ namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
[View(typeof(CivitAiBrowserPage))]
[Singleton]
public partial class CivitAiBrowserViewModel : TabViewModelBase
public partial class CivitAiBrowserViewModel : TabViewModelBase, IInfinitelyScroll
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ICivitApi civitApi;
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly ILiteDbContext liteDbContext;
private readonly INotificationService notificationService;
private const int MaxModelsPerPage = 20;
private LRUCache<
int /* model id */
,
CheckpointBrowserCardViewModel
> cache = new(50);
> cache = new(150);
[ObservableProperty]
private ObservableCollection<CheckpointBrowserCardViewModel>? modelCards;
private ObservableCollection<CheckpointBrowserCardViewModel> modelCards = new();
[ObservableProperty]
private DataGridCollectionView? modelCardsView;
@ -81,27 +81,9 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
[ObservableProperty]
private CivitModelType selectedModelType = CivitModelType.Checkpoint;
[ObservableProperty]
private int currentPageNumber;
[ObservableProperty]
private int totalPages;
[ObservableProperty]
private bool hasSearched;
[ObservableProperty]
private bool canGoToNextPage;
[ObservableProperty]
private bool canGoToPreviousPage;
[ObservableProperty]
private bool canGoToFirstPage;
[ObservableProperty]
private bool canGoToLastPage;
[ObservableProperty]
private bool isIndeterminate;
@ -117,7 +99,8 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
[ObservableProperty]
private bool showSantaHats = true;
private List<CheckpointBrowserCardViewModel> allModelCards = new();
[ObservableProperty]
private string? nextPageCursor;
public IEnumerable<CivitPeriod> AllCivitPeriods =>
Enum.GetValues(typeof(CivitPeriod)).Cast<CivitPeriod>();
@ -143,25 +126,11 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
)
{
this.civitApi = civitApi;
this.downloadService = downloadService;
this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory;
this.liteDbContext = liteDbContext;
this.notificationService = notificationService;
CurrentPageNumber = 1;
CanGoToNextPage = true;
CanGoToLastPage = true;
Observable
.FromEventPattern<PropertyChangedEventArgs>(this, nameof(PropertyChanged))
.Where(x => x.EventArgs.PropertyName == nameof(CurrentPageNumber))
.Throttle(TimeSpan.FromMilliseconds(250))
.Select<EventPattern<PropertyChangedEventArgs>, int>(_ => CurrentPageNumber)
.Where(page => page <= TotalPages && page > 0)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(_ => TrySearchAgain(false).SafeFireAndForget(), err => Logger.Error(err));
EventManager.Instance.NavigateAndFindCivitModelRequested += OnNavigateAndFindCivitModelRequested;
}
@ -171,7 +140,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
return;
SearchQuery = $"$#{e}";
SearchModelsCommand.ExecuteAsync(null).SafeFireAndForget();
SearchModelsCommand.ExecuteAsync(false).SafeFireAndForget();
}
public override void OnLoaded()
@ -208,6 +177,8 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
model => model.ShowNsfw,
settings => settings.ModelBrowserNsfwEnabled
);
SearchModelsCommand.ExecuteAsync(false);
}
/// <summary>
@ -223,14 +194,44 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
/// <summary>
/// Background update task
/// </summary>
private async Task CivitModelQuery(CivitModelsRequest request)
private async Task CivitModelQuery(CivitModelsRequest request, bool isInfiniteScroll = false)
{
var timer = Stopwatch.StartNew();
var queryText = request.Query;
var models = new List<CivitModel>();
CivitModelsResponse? modelsResponse = null;
try
{
var modelsResponse = await civitApi.GetModels(request);
var models = modelsResponse.Items;
if (!string.IsNullOrWhiteSpace(request.CommaSeparatedModelIds))
{
// count IDs
var ids = request.CommaSeparatedModelIds.Split(',');
if (ids.Length > 100)
{
var idChunks = ids.Chunk(100);
foreach (var chunk in idChunks)
{
request.CommaSeparatedModelIds = string.Join(",", chunk);
var chunkModelsResponse = await civitApi.GetModels(request);
if (chunkModelsResponse.Items != null)
{
models.AddRange(chunkModelsResponse.Items);
}
}
}
else
{
modelsResponse = await civitApi.GetModels(request);
models = modelsResponse.Items;
}
}
else
{
modelsResponse = await civitApi.GetModels(request);
models = modelsResponse.Items;
}
if (models is null)
{
Logger.Debug(
@ -272,19 +273,13 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
InsertedAt = DateTimeOffset.UtcNow,
Request = request,
Items = models,
Metadata = modelsResponse.Metadata
Metadata = modelsResponse?.Metadata
}
);
if (cacheNew)
{
Logger.Debug("New cache entry, updating model cards");
UpdateModelCards(models, modelsResponse.Metadata);
}
else
{
Logger.Debug("Cache entry already exists, not updating model cards");
}
UpdateModelCards(models, isInfiniteScroll);
NextPageCursor = modelsResponse?.Metadata?.NextCursor;
}
catch (OperationCanceledException)
{
@ -327,7 +322,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
/// <summary>
/// Updates model cards using api response object.
/// </summary>
private void UpdateModelCards(IEnumerable<CivitModel>? models, CivitMetadata? metadata)
private void UpdateModelCards(List<CivitModel>? models, bool addCards = false)
{
if (models is null)
{
@ -335,7 +330,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
}
else
{
var updateCards = models
var modelsToAdd = models
.Select(model =>
{
var cachedViewModel = cache.Get(model.Id);
@ -364,23 +359,34 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
return newCard;
})
.ToList();
.Where(FilterModelCardsPredicate);
allModelCards = updateCards;
var filteredCards = updateCards.Where(FilterModelCardsPredicate);
if (SortMode == CivitSortMode.Installed)
{
filteredCards = filteredCards.OrderByDescending(x => x.UpdateCardText == "Update Available");
modelsToAdd = modelsToAdd.OrderByDescending(x => x.UpdateCardText == "Update Available");
}
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(filteredCards);
if (!addCards)
{
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(modelsToAdd);
}
else
{
foreach (var model in modelsToAdd)
{
if (
ModelCards.Contains(
model,
new PropertyComparer<CheckpointBrowserCardViewModel>(x => x.CivitModel.Id)
)
)
continue;
ModelCards.Add(model);
}
}
}
TotalPages = metadata?.TotalPages ?? 1;
CanGoToFirstPage = CurrentPageNumber != 1;
CanGoToPreviousPage = CurrentPageNumber > 1;
CanGoToNextPage = CurrentPageNumber < TotalPages;
CanGoToLastPage = CurrentPageNumber != TotalPages;
// Status update
ShowMainLoadingSpinner = false;
IsIndeterminate = false;
@ -390,27 +396,30 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
private string previousSearchQuery = string.Empty;
[RelayCommand]
private async Task SearchModels()
private async Task SearchModels(bool isInfiniteScroll = false)
{
var timer = Stopwatch.StartNew();
if (SearchQuery != previousSearchQuery)
if (SearchQuery != previousSearchQuery || !isInfiniteScroll)
{
// Reset page number
CurrentPageNumber = 1;
previousSearchQuery = SearchQuery;
NextPageCursor = null;
}
// Build request
var modelRequest = new CivitModelsRequest
{
Limit = MaxModelsPerPage,
Nsfw = "true", // Handled by local view filter
Sort = SortMode,
Period = SelectedPeriod,
Page = CurrentPageNumber
Period = SelectedPeriod
};
if (NextPageCursor != null)
{
modelRequest.Cursor = NextPageCursor;
}
if (SelectedModelType != CivitModelType.All)
{
modelRequest.Types = [SelectedModelType];
@ -451,12 +460,14 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
{
var connectedModels = CheckpointFile
.GetAllCheckpointFiles(settingsManager.ModelsDirectory)
.Where(c => c.IsConnectedModel);
.Where(c => c.IsConnectedModel)
.ToList();
modelRequest.CommaSeparatedModelIds = string.Join(
",",
connectedModels.Select(c => c.ConnectedModel!.ModelId).GroupBy(m => m).Select(g => g.First())
);
modelRequest.Sort = null;
modelRequest.Period = null;
}
@ -516,14 +527,15 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
modelRequest.GetHashCode(),
elapsed.TotalSeconds
);
UpdateModelCards(cachedQuery.Items, cachedQuery.Metadata);
NextPageCursor = cachedQuery.Metadata?.NextCursor;
UpdateModelCards(cachedQuery.Items, isInfiniteScroll);
// Start remote query (background mode)
// Skip when last query was less than 2 min ago
var timeSinceCache = DateTimeOffset.UtcNow - cachedQuery.InsertedAt;
if (timeSinceCache?.TotalMinutes >= 2)
{
CivitModelQuery(modelRequest).SafeFireAndForget();
CivitModelQuery(modelRequest, isInfiniteScroll).SafeFireAndForget();
Logger.Debug(
"Cached query was more than 2 minutes ago ({Seconds:F0} s), updating cache with remote query",
timeSinceCache.Value.TotalSeconds
@ -534,54 +546,23 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
{
// Not cached, wait for remote query
ShowMainLoadingSpinner = true;
await CivitModelQuery(modelRequest);
await CivitModelQuery(modelRequest, isInfiniteScroll);
}
UpdateResultsText();
}
public void FirstPage()
{
CurrentPageNumber = 1;
}
public void PreviousPage()
{
if (CurrentPageNumber == 1)
return;
CurrentPageNumber--;
}
public void NextPage()
{
if (CurrentPageNumber == TotalPages)
return;
CurrentPageNumber++;
}
public void LastPage()
{
CurrentPageNumber = TotalPages;
}
public void ClearSearchQuery()
{
SearchQuery = string.Empty;
}
partial void OnShowNsfwChanged(bool value)
public async Task LoadNextPageAsync()
{
settingsManager.Transaction(s => s.ModelBrowserNsfwEnabled, value);
// ModelCardsView?.Refresh();
var updateCards = allModelCards.Where(FilterModelCardsPredicate);
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(updateCards);
if (!HasSearched)
return;
UpdateResultsText();
if (NextPageCursor != null)
{
await SearchModelsCommand.ExecuteAsync(true);
}
}
partial void OnSelectedPeriodChanged(CivitPeriod value)
@ -596,6 +577,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
SelectedBaseModelType
)
);
NextPageCursor = null;
}
partial void OnSortModeChanged(CivitSortMode value)
@ -610,6 +592,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
SelectedBaseModelType
)
);
NextPageCursor = null;
}
partial void OnSelectedModelTypeChanged(CivitModelType value)
@ -624,6 +607,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
SelectedBaseModelType
)
);
NextPageCursor = null;
}
partial void OnSelectedBaseModelTypeChanged(string value)
@ -638,6 +622,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
value
)
);
NextPageCursor = null;
}
private async Task TrySearchAgain(bool shouldUpdatePageNumber = true)
@ -648,18 +633,17 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
if (shouldUpdatePageNumber)
{
CurrentPageNumber = 1;
NextPageCursor = null;
}
// execute command instead of calling method directly so that the IsRunning property gets updated
await SearchModelsCommand.ExecuteAsync(null);
await SearchModelsCommand.ExecuteAsync(false);
}
private void UpdateResultsText()
{
NoResultsFound = ModelCards?.Count <= 0;
NoResultsText =
allModelCards.Count > 0 ? $"{allModelCards.Count} results hidden by filters" : "No results found";
NoResultsText = "No results found";
}
public override string Header => Resources.Label_CivitAi;

3
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

@ -6,6 +6,7 @@ using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.Views;
@ -20,7 +21,7 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[Singleton]
public partial class CheckpointBrowserViewModel : PageViewModelBase
{
public override string Title => "Model Browser";
public override string Title => Resources.Label_ModelBrowser;
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true };

20
StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs

@ -3,13 +3,14 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Avalonia.Data;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.DependencyInjection;
using NLog;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
@ -136,6 +137,12 @@ public partial class CheckpointFile : ViewModelBase
{
await Task.Run(() => File.Delete(cmInfoPath));
}
var settingsManager = App.Services.GetRequiredService<ISettingsManager>();
settingsManager.Transaction(s =>
{
s.InstalledModelHashes?.Remove(ConnectedModel.Hashes.BLAKE3);
});
}
}
catch (IOException ex)
@ -326,8 +333,15 @@ public partial class CheckpointFile : ViewModelBase
if (File.Exists(jsonPath))
{
var json = File.ReadAllText(jsonPath);
var connectedModelInfo = ConnectedModelInfo.FromJson(json);
checkpointFile.ConnectedModel = connectedModelInfo;
try
{
var connectedModelInfo = ConnectedModelInfo.FromJson(json);
checkpointFile.ConnectedModel = connectedModelInfo;
}
catch (JsonException)
{
checkpointFile.ConnectedModel = null;
}
}
checkpointFile.PreviewImagePath = SupportedImageExtensions

24
StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs

@ -15,6 +15,7 @@ using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
@ -92,7 +93,8 @@ public partial class CheckpointFolder : ViewModelBase
public string TitleWithFilesCount =>
CheckpointFiles.Any() || SubFolders.Any(f => f.CheckpointFiles.Any())
? $"{FolderType.GetDescription() ?? FolderType.GetStringValue()} ({CheckpointFiles.Count + SubFolders.Sum(folder => folder.CheckpointFiles.Count)})"
? $"{FolderType.GetDescription() ?? (string.IsNullOrWhiteSpace(FolderType.GetStringValue()) ? Path.GetFileName(DirectoryPath) : FolderType.GetStringValue())} "
+ $"({CheckpointFiles.Count + SubFolders.Sum(folder => folder.CheckpointFiles.Count)})"
: FolderType.GetDescription() ?? FolderType.GetStringValue();
public ProgressViewModel Progress { get; } = new();
@ -278,7 +280,7 @@ public partial class CheckpointFolder : ViewModelBase
var paths = files.Select(f => f.Path.LocalPath).ToArray();
await ImportFilesAsync(paths, settingsManager.Settings.IsImportAsConnected);
}
else if (e.Data.Get("Context") is CheckpointFile file)
else if (e.Data.GetContext<CheckpointFile>() is { } file)
{
await MoveBetweenFolders(file);
}
@ -447,6 +449,22 @@ public partial class CheckpointFolder : ViewModelBase
Progress.Value = 0;
var copyPaths = files.ToDictionary(k => k, v => Path.Combine(DirectoryPath, Path.GetFileName(v)));
// remove files that are already in the folder
foreach (var (source, destination) in copyPaths)
{
if (source == destination)
{
copyPaths.Remove(source);
}
}
if (copyPaths.Count == 0)
{
Progress.Text = "Import complete";
Progress.Value = 100;
return;
}
var progress = new Progress<ProgressReport>(report =>
{
Progress.IsIndeterminate = false;
@ -500,7 +518,7 @@ public partial class CheckpointFolder : ViewModelBase
await modelInfo.SaveJsonToDirectory(DirectoryPath, modelFileName);
// If available, save thumbnail
var image = version.Images?.FirstOrDefault();
var image = version.Images?.FirstOrDefault(x => x.Type == "image");
if (image != null)
{
var imageExt = Path.GetExtension(image.Url).TrimStart('.');

3
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

@ -25,6 +25,7 @@ using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
using Resources = StabilityMatrix.Avalonia.Languages.Resources;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource;
using TeachingTip = StabilityMatrix.Core.Models.Settings.TeachingTip;
@ -43,7 +44,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
private readonly INotificationService notificationService;
private readonly IMetadataImportService metadataImportService;
public override string Title => "Checkpoints";
public override string Title => Resources.Label_Checkpoints;
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Notebook, IsFilled = true };

15
StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@ -30,6 +31,7 @@ public partial class InferenceConnectionHelpViewModel : ContentDialogViewModelBa
private readonly ISettingsManager settingsManager;
private readonly INavigationService<MainWindowViewModel> navigationService;
private readonly IPackageFactory packageFactory;
private readonly RunningPackageService runningPackageService;
[ObservableProperty]
private string title = "Hello";
@ -58,12 +60,14 @@ public partial class InferenceConnectionHelpViewModel : ContentDialogViewModelBa
public InferenceConnectionHelpViewModel(
ISettingsManager settingsManager,
INavigationService<MainWindowViewModel> navigationService,
IPackageFactory packageFactory
IPackageFactory packageFactory,
RunningPackageService runningPackageService
)
{
this.settingsManager = settingsManager;
this.navigationService = navigationService;
this.packageFactory = packageFactory;
this.runningPackageService = runningPackageService;
// Get comfy type installed packages
var comfyPackages = this.settingsManager.Settings.InstalledPackages.Where(
@ -122,14 +126,11 @@ public partial class InferenceConnectionHelpViewModel : ContentDialogViewModelBa
/// Request launch of the selected package
/// </summary>
[RelayCommand]
private void LaunchSelectedPackage()
private async Task LaunchSelectedPackage()
{
if (SelectedPackage?.Id is { } id)
if (SelectedPackage is not null)
{
Dispatcher.UIThread.Post(() =>
{
EventManager.Instance.OnPackageLaunchRequested(id);
});
await runningPackageService.StartPackage(SelectedPackage);
}
}

11
StabilityMatrix.Avalonia/ViewModels/Dialogs/LykosLoginViewModel.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@ -90,7 +91,15 @@ public partial class LykosLoginViewModel : TaskDialogViewModelBase
}
catch (ApiException e)
{
LoginError = new AppException("Failed to login", $"{e.StatusCode} - {e.Message}");
LoginError = e.StatusCode switch
{
HttpStatusCode.Unauthorized
=> new AppException(
"Incorrect email or password",
"Please try again or reset your password"
),
_ => new AppException("Failed to login", $"{e.StatusCode} - {e.Message}")
};
}
}

26
StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs

@ -12,6 +12,7 @@ using DynamicData;
using DynamicData.Binding;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Models.PackageSteps;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
@ -75,7 +76,7 @@ public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase
.Connect()
.DeferUntilLoaded()
.Filter(incompatiblePredicate)
.Filter(p => p.OfferInOneClickInstaller || ShowIncompatiblePackages)
.Filter(p => p.OfferInOneClickInstaller)
.Sort(
SortExpressionComparer<BasePackage>
.Ascending(p => p.InstallerSortOrder)
@ -85,6 +86,10 @@ public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase
.Subscribe();
AllPackagesCache.AddOrUpdate(packageFactory.GetAllAvailablePackages());
if (ShownPackages.Count > 0)
return;
ShowIncompatiblePackages = true;
}
[RelayCommand]
@ -103,18 +108,19 @@ public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase
{
Task.Run(async () =>
{
var installLocation = Path.Combine(
settingsManager.LibraryDir,
"Packages",
selectedPackage.Name
);
var steps = new List<IPackageStep>
{
new SetPackageInstallingStep(settingsManager, selectedPackage.Name),
new SetupPrerequisitesStep(prerequisiteHelper, pyRunner, selectedPackage)
new SetupPrerequisitesStep(prerequisiteHelper, pyRunner, selectedPackage),
};
// get latest version & download & install
var installLocation = Path.Combine(
settingsManager.LibraryDir,
"Packages",
selectedPackage.Name
);
if (Directory.Exists(installLocation))
{
var installPath = new DirectoryPath(installLocation);
@ -144,6 +150,11 @@ public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase
);
steps.Add(downloadStep);
var unpackSiteCustomizeStep = new UnpackSiteCustomizeStep(
Path.Combine(installLocation, "venv")
);
steps.Add(unpackSiteCustomizeStep);
var installStep = new InstallPackageStep(
selectedPackage,
torchVersion,
@ -182,6 +193,7 @@ public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase
{
ShowDialogOnStart = false,
HideCloseButton = false,
ModificationCompleteMessage = $"{selectedPackage.DisplayName} installed successfully"
};
runner

7
StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs

@ -192,7 +192,12 @@ public partial class OneClickInstallViewModel : ContentDialogViewModelBase
var addInstalledPackageStep = new AddInstalledPackageStep(settingsManager, installedPackage);
steps.Add(addInstalledPackageStep);
var runner = new PackageModificationRunner { ShowDialogOnStart = true, HideCloseButton = true, };
var runner = new PackageModificationRunner
{
ShowDialogOnStart = true,
HideCloseButton = true,
ModificationCompleteMessage = $"{SelectedPackage.DisplayName} installed successfully"
};
EventManager.Instance.OnPackageInstallProgressAdded(runner);
await runner.ExecuteSteps(steps);

185
StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs

@ -0,0 +1,185 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.OpenArt;
using StabilityMatrix.Core.Models.Packages.Extensions;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(OpenArtWorkflowDialog))]
[ManagedService]
[Transient]
public partial class OpenArtWorkflowViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory
) : ContentDialogViewModelBase
{
public required OpenArtSearchResult Workflow { get; init; }
[ObservableProperty]
private ObservableCollection<OpenArtCustomNode> customNodes = [];
[ObservableProperty]
private string prunedDescription = string.Empty;
[ObservableProperty]
private bool installRequiredNodes = true;
[ObservableProperty]
private InstalledPackage? selectedPackage;
public PackagePair? SelectedPackagePair =>
SelectedPackage is { } package ? packageFactory.GetPackagePair(package) : null;
public List<InstalledPackage> AvailablePackages =>
settingsManager
.Settings.InstalledPackages.Where(package => package.PackageName == "ComfyUI")
.ToList();
public List<PackageExtension> MissingNodes { get; } = [];
public override async Task OnLoadedAsync()
{
if (Design.IsDesignMode)
return;
if (settingsManager.Settings.PreferredWorkflowPackage is { } preferredPackage)
{
SelectedPackage = preferredPackage;
}
else
{
SelectedPackage = AvailablePackages.FirstOrDefault();
}
if (SelectedPackage == null)
{
InstallRequiredNodes = false;
}
CustomNodes = new ObservableCollection<OpenArtCustomNode>(
await ParseNodes(Workflow.NodesIndex.ToList())
);
PrunedDescription = Utilities.RemoveHtml(Workflow.Description);
}
partial void OnSelectedPackageChanged(InstalledPackage? oldValue, InstalledPackage? newValue)
{
if (oldValue is null)
return;
settingsManager.Transaction(settings =>
{
settings.PreferredWorkflowPackage = newValue;
});
OnLoadedAsync().SafeFireAndForget();
}
[Localizable(false)]
private async Task<List<OpenArtCustomNode>> ParseNodes(List<string> nodes)
{
var indexOfFirstDot = nodes.IndexOf(".");
if (indexOfFirstDot != -1)
{
nodes = nodes[(indexOfFirstDot + 1)..];
}
var installedNodesNames = new HashSet<string>();
var nameToManifestNodes = new Dictionary<string, PackageExtension>();
var packagePair = SelectedPackagePair;
if (packagePair?.BasePackage.ExtensionManager is { } extensionManager)
{
var installedNodes = (
await extensionManager.GetInstalledExtensionsLiteAsync(packagePair.InstalledPackage)
).ToList();
var manifestExtensionsMap = await extensionManager.GetManifestExtensionsMapAsync(
extensionManager.GetManifests(packagePair.InstalledPackage)
);
// Add manifestExtensions definition to installedNodes if matching git repository url
installedNodes = installedNodes
.Select(installedNode =>
{
if (
installedNode.GitRepositoryUrl is not null
&& manifestExtensionsMap.TryGetValue(
installedNode.GitRepositoryUrl,
out var manifestExtension
)
)
{
installedNode = installedNode with { Definition = manifestExtension };
}
return installedNode;
})
.ToList();
// There may be duplicate titles, deduplicate by using the first one
nameToManifestNodes = manifestExtensionsMap
.GroupBy(x => x.Value.Title)
.ToDictionary(x => x.Key, x => x.First().Value);
installedNodesNames = installedNodes.Select(x => x.Title).ToHashSet();
}
var sections = new List<OpenArtCustomNode>();
OpenArtCustomNode? currentSection = null;
foreach (var node in nodes)
{
if (node is "." or ",")
{
currentSection = null; // End of the current section
continue;
}
if (currentSection == null)
{
currentSection = new OpenArtCustomNode
{
Title = node,
IsInstalled = installedNodesNames.Contains(node)
};
// Add missing nodes to the list
if (
!currentSection.IsInstalled && nameToManifestNodes.TryGetValue(node, out var manifestNode)
)
{
MissingNodes.Add(manifestNode);
}
sections.Add(currentSection);
}
else
{
currentSection.Children.Add(node);
}
}
if (sections.FirstOrDefault(x => x.Title == "ComfyUI") != null)
{
sections = sections.Where(x => x.Title != "ComfyUI").ToList();
}
return sections;
}
}

5
StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackagesItemViewModel.cs

@ -6,6 +6,7 @@ using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using Semver;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Python;
@ -114,6 +115,10 @@ public partial class PythonPackagesItemViewModel : ViewModelBase
}
}
}
catch (ProcessException)
{
// Ignore
}
finally
{
IsLoading = false;

4
StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelItemViewModel.cs

@ -22,9 +22,9 @@ public partial class RecommendedModelItemViewModel : ViewModelBase
private CivitModel civitModel;
public Uri ThumbnailUrl =>
ModelVersion.Images?.FirstOrDefault()?.Url == null
ModelVersion.Images?.FirstOrDefault(x => x.Type == "image")?.Url == null
? Assets.NoImage
: new Uri(ModelVersion.Images.First().Url);
: new Uri(ModelVersion.Images.First(x => x.Type == "image").Url);
[RelayCommand]
public void ToggleSelection() => IsSelected = !IsSelected;

13
StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs

@ -75,7 +75,7 @@ public partial class RecommendedModelsViewModel : ContentDialogViewModelBase
CivitModels
.Connect()
.DeferUntilLoaded()
.Filter(f => f.ModelVersion.BaseModel == "SDXL 1.0")
.Filter(f => f.ModelVersion.BaseModel == "SDXL 1.0" || f.ModelVersion.BaseModel == "Pony")
.Bind(SdxlModels)
.Subscribe();
}
@ -97,9 +97,11 @@ public partial class RecommendedModelsViewModel : ContentDialogViewModelBase
new RecommendedModelItemViewModel
{
ModelVersion = model.ModelVersions.First(
x => !x.BaseModel.Contains("Turbo", StringComparison.OrdinalIgnoreCase)
x =>
!x.BaseModel.Contains("Turbo", StringComparison.OrdinalIgnoreCase)
&& !x.BaseModel.Contains("Lightning", StringComparison.OrdinalIgnoreCase)
),
Author = $"by {model.Creator.Username}",
Author = $"by {model.Creator?.Username}",
CivitModel = model
}
)
@ -204,7 +206,10 @@ public partial class RecommendedModelsViewModel : ContentDialogViewModelBase
return null;
}
var image = modelVersion.Images[0];
var image = modelVersion.Images.FirstOrDefault(x => x.Type == "image");
if (image is null)
return null;
var imageExtension = Path.GetExtension(image.Url).TrimStart('.');
if (imageExtension is "jpg" or "jpeg" or "png")
{

25
StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs

@ -91,7 +91,6 @@ public partial class SelectModelVersionViewModel(
{
SelectedVersionViewModel = Versions[0];
CanGoToNextImage = true;
LoadInstallLocations();
}
partial void OnSelectedVersionViewModelChanged(ModelVersionViewModel? value)
@ -100,7 +99,7 @@ public partial class SelectModelVersionViewModel(
var allImages = value
?.ModelVersion
?.Images
?.Where(img => nsfwEnabled || img.Nsfw == "None")
?.Where(img => img.Type == "image" && (nsfwEnabled || img.NsfwLevel <= 1))
?.Select(x => new ImageSource(x.Url))
.ToList();
@ -130,6 +129,8 @@ public partial class SelectModelVersionViewModel(
var canImport = true;
if (settingsManager.IsLibraryDirSet)
{
LoadInstallLocations();
var fileSizeBytes = value?.CivitFile.SizeKb * 1024;
var freeSizeBytes =
SystemInfo.GetDiskFreeSpaceBytes(settingsManager.ModelsDirectory) ?? long.MaxValue;
@ -151,9 +152,9 @@ public partial class SelectModelVersionViewModel(
IsImportEnabled = value?.CivitFile != null && canImport && !ShowEmptyPathWarning;
}
partial void OnSelectedInstallLocationChanged(string value)
partial void OnSelectedInstallLocationChanged(string? value)
{
if (value.Equals("Custom...", StringComparison.OrdinalIgnoreCase))
if (value?.Equals("Custom...", StringComparison.OrdinalIgnoreCase) is true)
{
Dispatcher.UIThread.InvokeAsync(SelectCustomFolder);
}
@ -304,16 +305,20 @@ public partial class SelectModelVersionViewModel(
private void LoadInstallLocations()
{
var installLocations = new ObservableCollection<string>();
var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory);
var downloadDirectory = rootModelsDirectory.JoinDir(
CivitModel.Type.ConvertTo<SharedFolderType>().GetStringValue()
SelectedFile?.CivitFile.Type == CivitFileType.VAE
? SharedFolderType.VAE.GetStringValue()
: CivitModel.Type.ConvertTo<SharedFolderType>().GetStringValue()
);
var installLocations = new ObservableCollection<string>
{
downloadDirectory.ToString().Replace(rootModelsDirectory, "Models")
};
foreach (var directory in downloadDirectory.EnumerateDirectories())
installLocations.Add(downloadDirectory.ToString().Replace(rootModelsDirectory, "Models"));
foreach (
var directory in downloadDirectory.EnumerateDirectories(searchOption: SearchOption.AllDirectories)
)
{
installLocations.Add(directory.ToString().Replace(rootModelsDirectory, "Models"));
}

9
StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Styles;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views;
@ -27,11 +28,9 @@ public partial class FirstLaunchSetupViewModel : ViewModelBase
private RefreshBadgeViewModel checkHardwareBadge =
new()
{
WorkingToolTipText = "We're checking some hardware specifications to determine compatibility.",
SuccessToolTipText = "Everything looks good!",
FailToolTipText =
"We recommend a GPU with CUDA support for the best experience. "
+ "You can continue without one, but some packages may not work, and inference may be slower.",
WorkingToolTipText = Resources.Label_CheckingHardware,
SuccessToolTipText = Resources.Label_EverythingLooksGood,
FailToolTipText = Resources.Label_NvidiaGpuRecommended,
FailColorBrush = ThemeColors.ThemeYellow,
};

84
StabilityMatrix.Avalonia/ViewModels/Inference/ControlNetCardViewModel.cs

@ -1,19 +1,20 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
@ -32,7 +33,17 @@ public partial class ControlNetCardViewModel : LoadableViewModelBase
[ObservableProperty]
[Required]
private HybridModelFile? selectedPreprocessor;
private ComfyAuxPreprocessor? selectedPreprocessor;
[ObservableProperty]
[Required]
[Range(0, 2048)]
private int width;
[ObservableProperty]
[Required]
[Range(0, 2048)]
private int height;
[ObservableProperty]
[Required]
@ -62,6 +73,18 @@ public partial class ControlNetCardViewModel : LoadableViewModelBase
ClientManager = clientManager;
SelectImageCardViewModel = vmFactory.Get<SelectImageCardViewModel>();
// Update our width and height when the image changes
SelectImageCardViewModel
.WhenPropertyChanged(card => card.CurrentBitmapSize)
.Subscribe(propertyValue =>
{
if (!propertyValue.Value.IsEmpty)
{
Width = propertyValue.Value.Width;
Height = propertyValue.Value.Height;
}
});
}
[RelayCommand]
@ -79,4 +102,57 @@ public partial class ControlNetCardViewModel : LoadableViewModelBase
confirmDialog.StartDownload();
}
}
[RelayCommand]
private async Task PreviewPreprocessor(ComfyAuxPreprocessor? preprocessor)
{
if (
preprocessor is null
|| SelectImageCardViewModel.ImageSource is not { } imageSource
|| SelectImageCardViewModel.IsImageFileNotFound
)
return;
var args = new InferenceQueueCustomPromptEventArgs();
var images = SelectImageCardViewModel.GetInputImages();
await ClientManager.UploadInputImageAsync(imageSource);
var image = args.Nodes.AddTypedNode(
new ComfyNodeBuilder.LoadImage
{
Name = args.Nodes.GetUniqueName("Preprocessor_LoadImage"),
Image =
SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference")
?? throw new ValidationException("No ImageSource")
}
).Output1;
var aioPreprocessor = args.Nodes.AddTypedNode(
new ComfyNodeBuilder.AIOPreprocessor
{
Name = args.Nodes.GetUniqueName("Preprocessor"),
Image = image,
Preprocessor = preprocessor.ToString(),
Resolution = Width is <= 2048 and > 0 ? Width : 512
}
);
args.Builder.Connections.OutputNodes.Add(
args.Nodes.AddTypedNode(
new ComfyNodeBuilder.PreviewImage
{
Name = args.Nodes.GetUniqueName("Preprocessor_OutputImage"),
Images = aioPreprocessor.Output
}
)
);
// Queue
Dispatcher.UIThread.Post(() => EventManager.Instance.OnInferenceQueueCustomPrompt(args));
// We don't know when it's done so wait a bit?
await Task.Delay(1000);
}
}

36
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToImageViewModel.cs

@ -27,9 +27,17 @@ public class InferenceImageToImageViewModel : InferenceTextToImageViewModel
IInferenceClientManager inferenceClientManager,
INotificationService notificationService,
ISettingsManager settingsManager,
IModelIndexService modelIndexService
IModelIndexService modelIndexService,
RunningPackageService runningPackageService
)
: base(notificationService, inferenceClientManager, settingsManager, vmFactory, modelIndexService)
: base(
notificationService,
inferenceClientManager,
settingsManager,
vmFactory,
modelIndexService,
runningPackageService
)
{
SelectImageCardViewModel = vmFactory.Get<SelectImageCardViewModel>();
@ -48,26 +56,30 @@ public class InferenceImageToImageViewModel : InferenceTextToImageViewModel
_ => Convert.ToUInt64(SeedCardViewModel.Seed)
};
BatchSizeCardViewModel.ApplyStep(args);
var applyArgs = args.ToModuleApplyStepEventArgs();
BatchSizeCardViewModel.ApplyStep(applyArgs);
// Load models
ModelCardViewModel.ApplyStep(args);
ModelCardViewModel.ApplyStep(applyArgs);
// Setup image latent source
SelectImageCardViewModel.ApplyStep(args);
SelectImageCardViewModel.ApplyStep(applyArgs);
// Prompts and loras
PromptCardViewModel.ApplyStep(args);
PromptCardViewModel.ApplyStep(applyArgs);
// Setup Sampler and Refiner if enabled
SamplerCardViewModel.ApplyStep(args);
SamplerCardViewModel.ApplyStep(applyArgs);
// Apply module steps
foreach (var module in ModulesCardViewModel.Cards.OfType<ModuleBase>())
{
module.ApplyStep(args);
module.ApplyStep(applyArgs);
}
applyArgs.InvokeAllPreOutputActions();
builder.SetupOutputImage();
}
@ -77,12 +89,12 @@ public class InferenceImageToImageViewModel : InferenceTextToImageViewModel
var mainImages = SelectImageCardViewModel.GetInputImages();
var samplerImages = SamplerCardViewModel
.ModulesCardViewModel
.Cards
.OfType<IInputImageProvider>()
.ModulesCardViewModel.Cards.OfType<IInputImageProvider>()
.SelectMany(m => m.GetInputImages());
var moduleImages = ModulesCardViewModel.Cards.OfType<IInputImageProvider>().SelectMany(m => m.GetInputImages());
var moduleImages = ModulesCardViewModel
.Cards.OfType<IInputImageProvider>()
.SelectMany(m => m.GetInputImages());
return mainImages.Concat(samplerImages).Concat(moduleImages);
}

5
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs

@ -65,9 +65,10 @@ public partial class InferenceImageToVideoViewModel
IInferenceClientManager inferenceClientManager,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> vmFactory,
IModelIndexService modelIndexService
IModelIndexService modelIndexService,
RunningPackageService runningPackageService
)
: base(vmFactory, inferenceClientManager, notificationService, settingsManager)
: base(vmFactory, inferenceClientManager, notificationService, settingsManager, runningPackageService)
{
this.notificationService = notificationService;
this.modelIndexService = modelIndexService;

15
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs

@ -59,9 +59,10 @@ public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase
INotificationService notificationService,
IInferenceClientManager inferenceClientManager,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> vmFactory
ServiceManager<ViewModelBase> vmFactory,
RunningPackageService runningPackageService
)
: base(vmFactory, inferenceClientManager, notificationService, settingsManager)
: base(vmFactory, inferenceClientManager, notificationService, settingsManager, runningPackageService)
{
this.notificationService = notificationService;
@ -142,7 +143,10 @@ public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase
}
/// <inheritdoc />
protected override async Task GenerateImageImpl(GenerateOverrides overrides, CancellationToken cancellationToken)
protected override async Task GenerateImageImpl(
GenerateOverrides overrides,
CancellationToken cancellationToken
)
{
if (!ClientManager.IsConnected)
{
@ -169,7 +173,10 @@ public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase
Client = ClientManager.Client,
Nodes = buildPromptArgs.Builder.ToNodeDictionary(),
OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(),
Parameters = new GenerationParameters { ModelName = UpscalerCardViewModel.SelectedUpscaler?.Name, },
Parameters = new GenerationParameters
{
ModelName = UpscalerCardViewModel.SelectedUpscaler?.Name,
},
Project = InferenceProjectDocument.FromLoadable(this)
};

19
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs

@ -59,9 +59,10 @@ public class InferenceTextToImageViewModel : InferenceGenerationViewModelBase, I
IInferenceClientManager inferenceClientManager,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> vmFactory,
IModelIndexService modelIndexService
IModelIndexService modelIndexService,
RunningPackageService runningPackageService
)
: base(vmFactory, inferenceClientManager, notificationService, settingsManager)
: base(vmFactory, inferenceClientManager, notificationService, settingsManager, runningPackageService)
{
this.notificationService = notificationService;
this.modelIndexService = modelIndexService;
@ -130,10 +131,12 @@ public class InferenceTextToImageViewModel : InferenceGenerationViewModelBase, I
_ => Convert.ToUInt64(SeedCardViewModel.Seed)
};
BatchSizeCardViewModel.ApplyStep(args);
var applyArgs = args.ToModuleApplyStepEventArgs();
BatchSizeCardViewModel.ApplyStep(applyArgs);
// Load models
ModelCardViewModel.ApplyStep(args);
ModelCardViewModel.ApplyStep(applyArgs);
// Setup empty latent
builder.SetupEmptyLatentSource(
@ -144,17 +147,19 @@ public class InferenceTextToImageViewModel : InferenceGenerationViewModelBase, I
);
// Prompts and loras
PromptCardViewModel.ApplyStep(args);
PromptCardViewModel.ApplyStep(applyArgs);
// Setup Sampler and Refiner if enabled
SamplerCardViewModel.ApplyStep(args);
SamplerCardViewModel.ApplyStep(applyArgs);
// Hires fix if enabled
foreach (var module in ModulesCardViewModel.Cards.OfType<ModuleBase>())
{
module.ApplyStep(args);
module.ApplyStep(applyArgs);
}
applyArgs.InvokeAllPreOutputActions();
builder.SetupOutputImage();
}

100
StabilityMatrix.Avalonia/ViewModels/Inference/LayerDiffuseCardViewModel.cs

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using KGySoft.CoreLibraries;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Inference;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[Transient]
[ManagedService]
[View(typeof(LayerDiffuseCard))]
public partial class LayerDiffuseCardViewModel : LoadableViewModelBase, IComfyStep
{
public const string ModuleKey = "LayerDiffuse";
[ObservableProperty]
private LayerDiffuseMode selectedMode = LayerDiffuseMode.None;
public IEnumerable<LayerDiffuseMode> AvailableModes => Enum<LayerDiffuseMode>.GetValues();
[ObservableProperty]
[NotifyDataErrorInfo]
[Required]
[Range(-1d, 3d)]
private double weight = 1;
/// <inheritdoc />
public void ApplyStep(ModuleApplyStepEventArgs e)
{
if (SelectedMode == LayerDiffuseMode.None)
return;
var sdType = SelectedMode switch
{
LayerDiffuseMode.GenerateForegroundWithTransparencySD15 => "SD15",
LayerDiffuseMode.GenerateForegroundWithTransparencySDXL => "SDXL",
LayerDiffuseMode.None => throw new ArgumentOutOfRangeException(),
_ => throw new ArgumentOutOfRangeException()
};
// Choose config based on mode
var config = SelectedMode switch
{
LayerDiffuseMode.GenerateForegroundWithTransparencySD15
=> "SD15, Attention Injection, attn_sharing",
LayerDiffuseMode.GenerateForegroundWithTransparencySDXL => "SDXL, Conv Injection",
LayerDiffuseMode.None => throw new ArgumentOutOfRangeException(),
_ => throw new ArgumentOutOfRangeException()
};
foreach (var modelConnections in e.Temp.Models.Values)
{
var layerDiffuseApply = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.LayeredDiffusionApply
{
Name = e.Nodes.GetUniqueName($"LayerDiffuseApply_{modelConnections.Name}"),
Model = modelConnections.Model,
Config = config,
Weight = Weight,
}
);
modelConnections.Model = layerDiffuseApply.Output;
}
// Add pre output action
e.PreOutputActions.Add(applyArgs =>
{
// Use last latent for decode
var latent =
applyArgs.Builder.Connections.LastPrimaryLatent
?? throw new InvalidOperationException("Connections.LastPrimaryLatent not set");
// Convert primary to image if not already
var primaryImage = applyArgs.Builder.GetPrimaryAsImage();
applyArgs.Builder.Connections.Primary = primaryImage;
// Add a Layer Diffuse Decode
var decode = applyArgs.Nodes.AddTypedNode(
new ComfyNodeBuilder.LayeredDiffusionDecodeRgba
{
Name = applyArgs.Nodes.GetUniqueName("LayerDiffuseDecode"),
Samples = latent,
Images = primaryImage,
SdVersion = sdType
}
);
// Set primary to decode output
applyArgs.Builder.Connections.Primary = decode.Output;
});
}
}

119
StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs

@ -6,6 +6,9 @@ using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
@ -39,7 +42,7 @@ public class ControlNetModule : ModuleBase
{
var card = GetCard<ControlNetCardViewModel>();
var imageLoad = e.Nodes.AddTypedNode(
var image = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.LoadImage
{
Name = e.Nodes.GetUniqueName("ControlNet_LoadImage"),
@ -47,7 +50,101 @@ public class ControlNetModule : ModuleBase
card.SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference")
?? throw new ValidationException("No ImageSource")
}
);
).Output1;
if (card.SelectedPreprocessor is { } preprocessor && preprocessor != ComfyAuxPreprocessor.None)
{
var aioPreprocessor = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.AIOPreprocessor
{
Name = e.Nodes.GetUniqueName("ControlNet_Preprocessor"),
Image = image,
Preprocessor = preprocessor.ToString(),
// Use width if valid, else default of 512
Resolution = card.Width is <= 2048 and > 0 ? card.Width : 512
}
);
image = aioPreprocessor.Output;
}
// If ReferenceOnly is selected, use special node
if (card.SelectedModel == RemoteModels.ControlNetReferenceOnlyModel)
{
// We need to rescale image to be the current primary size if it's not already
var originalPrimary = e.Temp.Primary!.Unwrap();
var originalPrimarySize = e.Builder.Connections.PrimarySize;
if (card.SelectImageCardViewModel.CurrentBitmapSize != originalPrimarySize)
{
var scaled = e.Builder.Group_Upscale(
e.Nodes.GetUniqueName("ControlNet_Rescale"),
image,
e.Temp.GetDefaultVAE(),
ComfyUpscaler.NearestExact,
originalPrimarySize.Width,
originalPrimarySize.Height
);
e.Temp.Primary = scaled;
}
else
{
e.Temp.Primary = image;
}
// Set image as new latent source, add reference only node
var model = e.Temp.GetRefinerOrBaseModel();
var controlNetReferenceOnly = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.ReferenceOnlySimple
{
Name = e.Nodes.GetUniqueName("ControlNet_ReferenceOnly"),
Reference = e.Builder.GetPrimaryAsLatent(
e.Temp.Primary,
e.Builder.Connections.GetDefaultVAE()
),
Model = model
}
);
var referenceOnlyModel = controlNetReferenceOnly.Output1;
// If ControlNet strength is not 1, add Model Merge
if (Math.Abs(card.Strength - 1) > 0.01)
{
var modelBlend = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.ModelMergeSimple
{
Name = e.Nodes.GetUniqueName("ControlNet_ReferenceOnly_ModelMerge"),
Model1 = referenceOnlyModel,
Model2 = e.Temp.GetRefinerOrBaseModel(),
// Where 0 is full reference only, 1 is full original
Ratio = 1 - card.Strength
}
);
referenceOnlyModel = modelBlend.Output;
}
// Set output as new primary and model source
if (model == e.Temp.Refiner.Model)
{
e.Temp.Refiner.Model = referenceOnlyModel;
}
else
{
e.Temp.Base.Model = referenceOnlyModel;
}
e.Temp.Primary = controlNetReferenceOnly.Output2;
// Indicate that the Primary latent has been temp batched
// https://github.com/comfyanonymous/ComfyUI_experiments/issues/11
e.Temp.IsPrimaryTempBatched = true;
// Index 0 is the original image, index 1 is the reference only latent
e.Temp.PrimaryTempBatchPickIndex = 1;
return;
}
var controlNetLoader = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.ControlNetLoader
@ -62,36 +159,36 @@ public class ControlNetModule : ModuleBase
new ComfyNodeBuilder.ControlNetApplyAdvanced
{
Name = e.Nodes.GetUniqueName("ControlNetApply"),
Image = imageLoad.Output1,
Image = image,
ControlNet = controlNetLoader.Output,
Positive = e.Temp.Conditioning?.Positive ?? throw new ArgumentException("No Conditioning"),
Negative = e.Temp.Conditioning?.Negative ?? throw new ArgumentException("No Conditioning"),
Positive = e.Temp.Base.Conditioning!.Unwrap().Positive,
Negative = e.Temp.Base.Conditioning.Negative,
Strength = card.Strength,
StartPercent = card.StartPercent,
EndPercent = card.EndPercent,
}
);
e.Temp.Conditioning = (controlNetApply.Output1, controlNetApply.Output2);
e.Temp.Base.Conditioning = (controlNetApply.Output1, controlNetApply.Output2);
// Refiner if available
if (e.Temp.RefinerConditioning is not null)
if (e.Temp.Refiner.Conditioning is not null)
{
var controlNetRefinerApply = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.ControlNetApplyAdvanced
{
Name = e.Nodes.GetUniqueName("Refiner_ControlNetApply"),
Image = imageLoad.Output1,
Image = image,
ControlNet = controlNetLoader.Output,
Positive = e.Temp.RefinerConditioning.Positive,
Negative = e.Temp.RefinerConditioning.Negative,
Positive = e.Temp.Refiner.Conditioning!.Unwrap().Positive,
Negative = e.Temp.Refiner.Conditioning.Negative,
Strength = card.Strength,
StartPercent = card.StartPercent,
EndPercent = card.EndPercent,
}
);
e.Temp.RefinerConditioning = (controlNetRefinerApply.Output1, controlNetRefinerApply.Output2);
e.Temp.Refiner.Conditioning = (controlNetRefinerApply.Output1, controlNetRefinerApply.Output2);
}
}
}

57
StabilityMatrix.Avalonia/ViewModels/Inference/Modules/HiresFixModule.cs

@ -78,30 +78,39 @@ public partial class HiresFixModule : ModuleBase
);
}
var hiresSampler = builder
.Nodes
.AddTypedNode(
new ComfyNodeBuilder.KSampler
{
Name = builder.Nodes.GetUniqueName("HiresFix_Sampler"),
Model = builder.Connections.GetRefinerOrBaseModel(),
Seed = builder.Connections.Seed,
Steps = samplerCard.Steps,
Cfg = samplerCard.CfgScale,
SamplerName =
samplerCard.SelectedSampler?.Name
?? e.Builder.Connections.PrimarySampler?.Name
?? throw new ArgumentException("No PrimarySampler"),
Scheduler =
samplerCard.SelectedScheduler?.Name
?? e.Builder.Connections.PrimaryScheduler?.Name
?? throw new ArgumentException("No PrimaryScheduler"),
Positive = builder.Connections.GetRefinerOrBaseConditioning().Positive,
Negative = builder.Connections.GetRefinerOrBaseConditioning().Negative,
LatentImage = builder.GetPrimaryAsLatent(),
Denoise = samplerCard.DenoiseStrength
}
);
// If we need to inherit primary sampler addons, use their temp args
if (samplerCard.InheritPrimarySamplerAddons)
{
e.Temp = e.Builder.Connections.BaseSamplerTemporaryArgs ?? e.CreateTempFromBuilder();
}
else
{
// otherwise just use new ones
e.Temp = e.CreateTempFromBuilder();
}
var hiresSampler = builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.KSampler
{
Name = builder.Nodes.GetUniqueName("HiresFix_Sampler"),
Model = builder.Connections.GetRefinerOrBaseModel(),
Seed = builder.Connections.Seed,
Steps = samplerCard.Steps,
Cfg = samplerCard.CfgScale,
SamplerName =
samplerCard.SelectedSampler?.Name
?? e.Builder.Connections.PrimarySampler?.Name
?? throw new ArgumentException("No PrimarySampler"),
Scheduler =
samplerCard.SelectedScheduler?.Name
?? e.Builder.Connections.PrimaryScheduler?.Name
?? throw new ArgumentException("No PrimaryScheduler"),
Positive = e.Temp.GetRefinerOrBaseConditioning().Positive,
Negative = e.Temp.GetRefinerOrBaseConditioning().Negative,
LatentImage = builder.GetPrimaryAsLatent(),
Denoise = samplerCard.DenoiseStrength
}
);
// Set as primary
builder.Connections.Primary = hiresSampler.Output;

26
StabilityMatrix.Avalonia/ViewModels/Inference/Modules/LayerDiffuseModule.cs

@ -0,0 +1,26 @@
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
[ManagedService]
[Transient]
public class LayerDiffuseModule : ModuleBase
{
/// <inheritdoc />
public LayerDiffuseModule(ServiceManager<ViewModelBase> vmFactory)
: base(vmFactory)
{
Title = "Layer Diffuse";
AddCards(vmFactory.Get<LayerDiffuseCardViewModel>());
}
/// <inheritdoc />
protected override void OnApplyStep(ModuleApplyStepEventArgs e)
{
var card = GetCard<LayerDiffuseCardViewModel>();
card.ApplyStep(e);
}
}

30
StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs

@ -20,6 +20,7 @@ using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
@ -30,6 +31,7 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference;
public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoadableState, IComfyStep
{
private readonly IModelIndexService modelIndexService;
private readonly ISettingsManager settingsManager;
/// <summary>
/// Cache of prompt text to tokenized Prompt
@ -48,6 +50,9 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
[ObservableProperty]
private bool isAutoCompletionEnabled;
[ObservableProperty]
private bool isHelpButtonTeachingTipOpen;
/// <inheritdoc />
public PromptCardViewModel(
ICompletionProvider completionProvider,
@ -59,6 +64,7 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
)
{
this.modelIndexService = modelIndexService;
this.settingsManager = settingsManager;
CompletionProvider = completionProvider;
TokenizerProvider = tokenizerProvider;
SharedState = sharedState;
@ -77,6 +83,30 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
);
}
partial void OnIsHelpButtonTeachingTipOpenChanging(bool oldValue, bool newValue)
{
// If the teaching tip is being closed, save the setting
if (oldValue && !newValue)
{
settingsManager.Transaction(settings =>
{
settings.SeenTeachingTips.Add(TeachingTip.InferencePromptHelpButtonTip);
});
}
}
/// <inheritdoc />
public override void OnLoaded()
{
base.OnLoaded();
// Show teaching tip for help button if not seen
if (!settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.InferencePromptHelpButtonTip))
{
IsHelpButtonTeachingTipOpen = true;
}
}
/// <summary>
/// Applies the prompt step.
/// Requires:

75
StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs

@ -87,6 +87,11 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
[Required]
private ComfyScheduler? selectedScheduler = ComfyScheduler.Normal;
[ObservableProperty]
[property: Category("Settings")]
[property: DisplayName("Inherit Primary Sampler Addons")]
private bool inheritPrimarySamplerAddons = true;
[JsonPropertyName("Modules")]
public StackEditableCardViewModel ModulesCardViewModel { get; }
@ -104,7 +109,12 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
ModulesCardViewModel = vmFactory.Get<StackEditableCardViewModel>(modulesCard =>
{
modulesCard.Title = Resources.Label_Addons;
modulesCard.AvailableModules = [typeof(FreeUModule), typeof(ControlNetModule)];
modulesCard.AvailableModules =
[
typeof(FreeUModule),
typeof(ControlNetModule),
typeof(LayerDiffuseModule)
];
});
}
@ -130,8 +140,7 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
}
// Provide temp values
e.Temp.Conditioning = e.Builder.Connections.Base.Conditioning;
e.Temp.RefinerConditioning = e.Builder.Connections.Refiner.Conditioning;
e.Temp = e.CreateTempFromBuilder();
// Apply steps from our addons
ApplyAddonSteps(e);
@ -142,17 +151,26 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
if (!e.Nodes.ContainsKey("Sampler"))
{
ApplyStepsInitialSampler(e);
// Save temp
e.Builder.Connections.BaseSamplerTemporaryArgs = e.Temp;
}
else
{
ApplyStepsAdditionalSampler(e);
// Hires does its own sampling so just throw I guess
throw new InvalidOperationException(
"Sampler ApplyStep was called when Sampler node already exists"
);
}
}
private void ApplyStepsInitialSampler(ModuleApplyStepEventArgs e)
{
// Get primary as latent using vae
var primaryLatent = e.Builder.GetPrimaryAsLatent();
var primaryLatent = e.Builder.GetPrimaryAsLatent(
e.Temp.Primary!.Unwrap(),
e.Builder.Connections.GetDefaultVAE()
);
// Set primary sampler and scheduler
var primarySampler = SelectedSampler ?? throw new ValidationException("Sampler not selected");
@ -162,8 +180,8 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
e.Builder.Connections.PrimaryScheduler = primaryScheduler;
// Use Temp Conditioning that may be modified by addons
var conditioning = e.Temp.Conditioning.Unwrap();
var refinerConditioning = e.Temp.RefinerConditioning;
var conditioning = e.Temp.Base.Conditioning.Unwrap();
var refinerConditioning = e.Temp.Refiner.Conditioning;
// Use custom sampler if SDTurbo scheduler is selected
if (e.Builder.Connections.PrimaryScheduler == ComfyScheduler.SDTurbo)
@ -209,21 +227,16 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
);
e.Builder.Connections.Primary = sampler.Output1;
return;
}
// Use KSampler if no refiner, otherwise need KSamplerAdvanced
if (e.Builder.Connections.Refiner.Model is null)
else if (e.Builder.Connections.Refiner.Model is null)
{
var baseConditioning = e.Builder.Connections.Base.Conditioning.Unwrap();
// No refiner
var sampler = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.KSampler
{
Name = "Sampler",
Model = e.Builder.Connections.Base.Model.Unwrap(),
Model = e.Temp.Base.Model!.Unwrap(),
Seed = e.Builder.Connections.Seed,
SamplerName = primarySampler.Name,
Scheduler = primaryScheduler.Name,
@ -245,7 +258,7 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
new ComfyNodeBuilder.KSamplerAdvanced
{
Name = "Sampler",
Model = e.Builder.Connections.Base.Model.Unwrap(),
Model = e.Temp.Base.Model!.Unwrap(),
AddNoise = true,
NoiseSeed = e.Builder.Connections.Seed,
Steps = TotalSteps,
@ -261,8 +274,30 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
}
);
e.Builder.Connections.Primary = sampler.Output;
}
// If temp batched, add a LatentFromBatch to pick the temp batch right after first sampler
if (e.Temp.IsPrimaryTempBatched)
{
e.Builder.Connections.Primary = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.LatentFromBatch
{
Name = e.Nodes.GetUniqueName("ControlNet_LatentFromBatch"),
Samples = e.Builder.GetPrimaryAsLatent(),
BatchIndex = e.Temp.PrimaryTempBatchPickIndex,
// Use max length here as recommended
// https://github.com/comfyanonymous/ComfyUI_experiments/issues/11
Length = 64
}
).Output;
}
// Refiner
if (e.Builder.Connections.Refiner.Model is not null)
{
// Add refiner sampler
var refinerSampler = e.Nodes.AddTypedNode(
e.Builder.Connections.Primary = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.KSamplerAdvanced
{
Name = "Sampler_Refiner",
@ -276,19 +311,15 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
Positive = refinerConditioning!.Positive,
Negative = refinerConditioning.Negative,
// Connect to previous sampler
LatentImage = sampler.Output,
LatentImage = e.Builder.GetPrimaryAsLatent(),
StartAtStep = Steps,
EndAtStep = TotalSteps,
ReturnWithLeftoverNoise = false
}
);
e.Builder.Connections.Primary = refinerSampler.Output;
).Output;
}
}
private void ApplyStepsAdditionalSampler(ModuleApplyStepEventArgs e) { }
/// <summary>
/// Applies each step of our addons
/// </summary>

144
StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Linq;
using System.Text.Json;
@ -16,6 +18,7 @@ using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using NLog;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
@ -51,8 +54,10 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
private readonly ServiceManager<ViewModelBase> vmFactory;
private readonly IModelIndexService modelIndexService;
private readonly ILiteDbContext liteDbContext;
private readonly RunningPackageService runningPackageService;
private Guid? selectedPackageId;
public override string Title => "Inference";
public override string Title => Resources.Label_Inference;
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.AppGeneric, IsFilled = true };
@ -62,7 +67,7 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
State = ProgressState.Failed,
FailToolTipText = "Not connected",
FailIcon = FluentAvalonia.UI.Controls.Symbol.Refresh,
SuccessToolTipText = "Connected",
SuccessToolTipText = Resources.Label_Connected,
};
public IInferenceClientManager ClientManager { get; }
@ -86,6 +91,8 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
public bool IsComfyRunning => RunningPackage?.BasePackage is ComfyUI;
private IDisposable? onStartupComplete;
public InferenceViewModel(
ServiceManager<ViewModelBase> vmFactory,
INotificationService notificationService,
@ -93,6 +100,7 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
ISettingsManager settingsManager,
IModelIndexService modelIndexService,
ILiteDbContext liteDbContext,
RunningPackageService runningPackageService,
SharedState sharedState
)
{
@ -101,12 +109,13 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
this.settingsManager = settingsManager;
this.modelIndexService = modelIndexService;
this.liteDbContext = liteDbContext;
this.runningPackageService = runningPackageService;
ClientManager = inferenceClientManager;
SharedState = sharedState;
// Keep RunningPackage updated with the current package pair
EventManager.Instance.RunningPackageStatusChanged += OnRunningPackageStatusChanged;
runningPackageService.RunningPackages.CollectionChanged += RunningPackagesOnCollectionChanged;
// "Send to Inference"
EventManager.Instance.InferenceTextToImageRequested += OnInferenceTextToImageRequested;
@ -114,58 +123,112 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
EventManager.Instance.InferenceImageToImageRequested += OnInferenceImageToImageRequested;
EventManager.Instance.InferenceImageToVideoRequested += OnInferenceImageToVideoRequested;
// Global requests for custom prompt queueing
EventManager.Instance.InferenceQueueCustomPrompt += OnInferenceQueueCustomPromptRequested;
MenuSaveAsCommand.WithConditionalNotificationErrorHandler(notificationService);
MenuOpenProjectCommand.WithConditionalNotificationErrorHandler(notificationService);
}
private void DisconnectFromComfy()
{
RunningPackage = null;
// Cancel any pending connection
if (ConnectCancelCommand.CanExecute(null))
{
ConnectCancelCommand.Execute(null);
}
onStartupComplete?.Dispose();
onStartupComplete = null;
IsWaitingForConnection = false;
// Disconnect
Logger.Trace("On package close - disconnecting");
DisconnectCommand.Execute(null);
}
/// <summary>
/// Updates the RunningPackage property when the running package changes.
/// Also starts a connection to the backend if a new ComfyUI package is running.
/// And disconnects if the package is closed.
/// </summary>
private void OnRunningPackageStatusChanged(object? sender, RunningPackageStatusChangedEventArgs e)
private void RunningPackagesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RunningPackage = e.CurrentPackagePair;
if (
e.NewItems?.OfType<KeyValuePair<Guid, RunningPackageViewModel>>().Select(x => x.Value)
is not { } newItems
)
{
if (RunningPackage != null)
{
DisconnectFromComfy();
}
return;
}
IDisposable? onStartupComplete = null;
var comfyViewModel = newItems.FirstOrDefault(
vm =>
vm.RunningPackage.InstalledPackage.Id == selectedPackageId
|| vm.RunningPackage.BasePackage is ComfyUI
);
Dispatcher.UIThread.Post(() =>
if (comfyViewModel is null && RunningPackage?.BasePackage is ComfyUI)
{
if (e.CurrentPackagePair?.BasePackage is ComfyUI package)
{
IsWaitingForConnection = true;
onStartupComplete = Observable
.FromEventPattern<string>(package, nameof(package.StartupComplete))
.Take(1)
.Subscribe(_ =>
DisconnectFromComfy();
}
else if (comfyViewModel != null && RunningPackage == null)
{
IsWaitingForConnection = true;
RunningPackage = comfyViewModel.RunningPackage;
onStartupComplete = Observable
.FromEventPattern<string>(
comfyViewModel.RunningPackage.BasePackage,
nameof(comfyViewModel.RunningPackage.BasePackage.StartupComplete)
)
.Take(1)
.Subscribe(_ =>
{
Dispatcher.UIThread.Post(() =>
{
Dispatcher.UIThread.Post(() =>
if (ConnectCommand.CanExecute(null))
{
if (ConnectCommand.CanExecute(null))
{
Logger.Trace("On package launch - starting connection");
ConnectCommand.Execute(null);
}
IsWaitingForConnection = false;
});
Logger.Trace("On package launch - starting connection");
ConnectCommand.Execute(null);
}
IsWaitingForConnection = false;
});
}
else
{
// Cancel any pending connection
if (ConnectCancelCommand.CanExecute(null))
});
}
}
private void OnInferenceQueueCustomPromptRequested(object? sender, InferenceQueueCustomPromptEventArgs e)
{
// Get currently selected tab
var currentTab = SelectedTab;
if (currentTab is InferenceGenerationViewModelBase generationViewModel)
{
Dispatcher
.UIThread.InvokeAsync(async () =>
{
ConnectCancelCommand.Execute(null);
}
onStartupComplete?.Dispose();
onStartupComplete = null;
IsWaitingForConnection = false;
await generationViewModel.RunCustomGeneration(e);
})
.SafeFireAndForget(ex =>
{
Logger.Error(ex, "Failed to queue prompt");
// Disconnect
Logger.Trace("On package close - disconnecting");
DisconnectCommand.Execute(null);
}
});
Dispatcher.UIThread.Post(() =>
{
notificationService.ShowPersistent(
"Failed to queue prompt",
$"{ex.GetType().Name}: {ex.Message}",
NotificationType.Error
);
});
});
}
}
public override void OnLoaded()
@ -390,7 +453,12 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
private async Task ShowConnectionHelp()
{
var vm = vmFactory.Get<InferenceConnectionHelpViewModel>();
await vm.CreateDialog().ShowAsync();
var result = await vm.CreateDialog().ShowAsync();
if (result != ContentDialogResult.Primary)
return;
selectedPackageId = vm.SelectedPackage?.Id;
}
/// <summary>

173
StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs

@ -0,0 +1,173 @@
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Api.OpenArt;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(InstalledWorkflowsPage))]
[Singleton]
public partial class InstalledWorkflowsViewModel(
ISettingsManager settingsManager,
INotificationService notificationService
) : TabViewModelBase, IDisposable
{
public override string Header => Resources.TabLabel_InstalledWorkflows;
private readonly SourceCache<OpenArtMetadata, string> workflowsCache =
new(x => x.Workflow?.Id ?? Guid.NewGuid().ToString());
[ObservableProperty]
private IObservableCollection<OpenArtMetadata> displayedWorkflows =
new ObservableCollectionExtended<OpenArtMetadata>();
protected override async Task OnInitialLoadedAsync()
{
await base.OnInitialLoadedAsync();
workflowsCache.Connect().DeferUntilLoaded().Bind(DisplayedWorkflows).Subscribe();
if (Design.IsDesignMode)
return;
await LoadInstalledWorkflowsAsync();
EventManager.Instance.WorkflowInstalled += OnWorkflowInstalled;
}
[RelayCommand]
private async Task LoadInstalledWorkflowsAsync()
{
workflowsCache.Clear();
if (!Directory.Exists(settingsManager.WorkflowDirectory))
{
Directory.CreateDirectory(settingsManager.WorkflowDirectory);
}
foreach (
var workflowPath in Directory.EnumerateFiles(
settingsManager.WorkflowDirectory,
"*.json",
SearchOption.AllDirectories
)
)
{
try
{
var json = await File.ReadAllTextAsync(workflowPath);
var metadata = JsonSerializer.Deserialize<OpenArtMetadata>(json);
if (metadata?.Workflow == null)
{
metadata = new OpenArtMetadata
{
Workflow = new OpenArtSearchResult
{
Id = Guid.NewGuid().ToString(),
Name = Path.GetFileNameWithoutExtension(workflowPath)
}
};
}
metadata.FilePath = [await App.StorageProvider.TryGetFileFromPathAsync(workflowPath)];
workflowsCache.AddOrUpdate(metadata);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
[RelayCommand]
private async Task OpenInExplorer(OpenArtMetadata metadata)
{
if (metadata.FilePath == null)
return;
var path = metadata.FilePath.FirstOrDefault()?.Path.ToString();
if (string.IsNullOrWhiteSpace(path))
return;
await ProcessRunner.OpenFileBrowser(path);
}
[RelayCommand]
private void OpenOnOpenArt(OpenArtMetadata metadata)
{
if (metadata.Workflow == null)
return;
ProcessRunner.OpenUrl($"https://openart.ai/workflows/{metadata.Workflow.Id}");
}
[RelayCommand]
private async Task DeleteAsync(OpenArtMetadata metadata)
{
var confirmationDialog = new BetterContentDialog
{
Title = Resources.Label_AreYouSure,
Content = Resources.Label_ActionCannotBeUndone,
PrimaryButtonText = Resources.Action_Delete,
SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary,
IsSecondaryButtonEnabled = true,
};
var dialogResult = await confirmationDialog.ShowAsync();
if (dialogResult != ContentDialogResult.Primary)
return;
await using var delay = new MinimumDelay(200, 500);
var path = metadata?.FilePath?.FirstOrDefault()?.Path.ToString().Replace("file:///", "");
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
{
await notificationService.TryAsync(
Task.Run(() => File.Delete(path)),
message: "Error deleting workflow"
);
var id = metadata?.Workflow?.Id;
if (!string.IsNullOrWhiteSpace(id))
{
workflowsCache.Remove(id);
}
}
notificationService.Show(
Resources.Label_WorkflowDeleted,
string.Format(Resources.Label_WorkflowDeletedSuccessfully, metadata?.Workflow?.Name)
);
}
private void OnWorkflowInstalled(object? sender, EventArgs e)
{
LoadInstalledWorkflowsAsync().SafeFireAndForget();
}
public void Dispose()
{
workflowsCache.Dispose();
EventManager.Instance.WorkflowInstalled -= OnWorkflowInstalled;
}
}

33
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
@ -36,6 +37,7 @@ public partial class MainWindowViewModel : ViewModelBase
private readonly ITrackedDownloadService trackedDownloadService;
private readonly IDiscordRichPresenceService discordRichPresenceService;
private readonly IModelIndexService modelIndexService;
private readonly Lazy<IModelDownloadLinkHandler> modelDownloadLinkHandler;
public string Greeting => "Welcome to Avalonia!";
[ObservableProperty]
@ -71,7 +73,8 @@ public partial class MainWindowViewModel : ViewModelBase
IDiscordRichPresenceService discordRichPresenceService,
ServiceManager<ViewModelBase> dialogFactory,
ITrackedDownloadService trackedDownloadService,
IModelIndexService modelIndexService
IModelIndexService modelIndexService,
Lazy<IModelDownloadLinkHandler> modelDownloadLinkHandler
)
{
this.settingsManager = settingsManager;
@ -79,6 +82,7 @@ public partial class MainWindowViewModel : ViewModelBase
this.discordRichPresenceService = discordRichPresenceService;
this.trackedDownloadService = trackedDownloadService;
this.modelIndexService = modelIndexService;
this.modelDownloadLinkHandler = modelDownloadLinkHandler;
ProgressManagerViewModel = dialogFactory.Get<ProgressManagerViewModel>();
UpdateViewModel = dialogFactory.Get<UpdateViewModel>();
}
@ -107,6 +111,25 @@ public partial class MainWindowViewModel : ViewModelBase
return;
}
try
{
await modelDownloadLinkHandler.Value.StartListening();
}
catch (IOException)
{
var dialog = new BetterContentDialog
{
Title = Resources.Label_StabilityMatrixAlreadyRunning,
Content = Resources.Label_AnotherInstanceAlreadyRunning,
IsPrimaryButtonEnabled = true,
PrimaryButtonText = Resources.Action_Close,
DefaultButton = ContentDialogButton.Primary
};
await dialog.ShowAsync();
App.Shutdown();
return;
}
// Initialize Discord Rich Presence (this needs LibraryDir so is set here)
discordRichPresenceService.UpdateState();
@ -139,14 +162,6 @@ public partial class MainWindowViewModel : ViewModelBase
Content = new NewOneClickInstallDialog { DataContext = viewModel },
};
EventManager.Instance.OneClickInstallFinished += (_, skipped) =>
{
if (skipped)
return;
EventManager.Instance.OnTeachingTooltipNeeded();
};
var firstDialogResult = await dialog.ShowAsync(App.TopLevel);
if (firstDialogResult != ContentDialogResult.Primary)

3
StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs

@ -14,6 +14,7 @@ using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using Refit;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
@ -47,7 +48,7 @@ public partial class NewCheckpointsPageViewModel(
IMetadataImportService metadataImportService
) : PageViewModelBase
{
public override string Title => "Checkpoint Manager";
public override string Title => Resources.Label_CheckpointManager;
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Cellular5g, IsFilled = true };

7
StabilityMatrix.Avalonia/ViewModels/NewPackageManagerViewModel.cs

@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
@ -18,7 +19,7 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[Singleton]
public partial class NewPackageManagerViewModel : PageViewModelBase
{
public override string Title => "Packages";
public override string Title => Resources.Label_Packages;
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Box, IsFilled = true };
public IReadOnlyList<PageViewModelBase> SubPages { get; }
@ -58,6 +59,10 @@ public partial class NewPackageManagerViewModel : PageViewModelBase
{
CurrentPagePath.Add(value);
}
else if (value is RunningPackageViewModel)
{
CurrentPagePath.Add(value);
}
else
{
CurrentPagePath.Clear();

347
StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs

@ -0,0 +1,347 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using Refit;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models.Api.OpenArt;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
using Resources = StabilityMatrix.Avalonia.Languages.Resources;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(OpenArtBrowserPage))]
[Singleton]
public partial class OpenArtBrowserViewModel(
IOpenArtApi openArtApi,
INotificationService notificationService,
ISettingsManager settingsManager,
IPackageFactory packageFactory
) : TabViewModelBase, IInfinitelyScroll
{
private const int PageSize = 20;
public override string Header => Resources.Label_OpenArtBrowser;
private readonly SourceCache<OpenArtSearchResult, string> searchResultsCache = new(x => x.Id);
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PageCount), nameof(CanGoBack), nameof(CanGoForward), nameof(CanGoToEnd))]
private OpenArtSearchResponse? latestSearchResponse;
[ObservableProperty]
private IObservableCollection<OpenArtSearchResult> searchResults =
new ObservableCollectionExtended<OpenArtSearchResult>();
[ObservableProperty]
private string searchQuery = string.Empty;
[ObservableProperty]
private bool isLoading;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(InternalPageNumber), nameof(CanGoBack))]
private int displayedPageNumber = 1;
public int InternalPageNumber => DisplayedPageNumber - 1;
public int PageCount =>
Math.Max(
1,
Convert.ToInt32(Math.Ceiling((LatestSearchResponse?.Total ?? 0) / Convert.ToDouble(PageSize)))
);
public bool CanGoBack =>
string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) && InternalPageNumber > 0;
public bool CanGoForward =>
!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) || PageCount > InternalPageNumber + 1;
public bool CanGoToEnd =>
string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) && PageCount > InternalPageNumber + 1;
public IEnumerable<string> AllSortModes => ["Trending", "Latest", "Most Downloaded", "Most Liked"];
[ObservableProperty]
private string? selectedSortMode;
protected override void OnInitialLoaded()
{
searchResultsCache.Connect().DeferUntilLoaded().Bind(SearchResults).Subscribe();
SelectedSortMode = AllSortModes.First();
DoSearch().SafeFireAndForget();
}
[RelayCommand]
private async Task FirstPage()
{
DisplayedPageNumber = 1;
searchResultsCache.Clear();
await DoSearch();
}
[RelayCommand]
private async Task PreviousPage()
{
DisplayedPageNumber--;
searchResultsCache.Clear();
await DoSearch(InternalPageNumber);
}
[RelayCommand]
private async Task NextPage()
{
if (string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor))
{
DisplayedPageNumber++;
}
searchResultsCache.Clear();
await DoSearch(InternalPageNumber);
}
[RelayCommand]
private async Task LastPage()
{
if (string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor))
{
DisplayedPageNumber = PageCount;
}
searchResultsCache.Clear();
await DoSearch(PageCount - 1);
}
[Localizable(false)]
[RelayCommand]
private void OpenModel(OpenArtSearchResult workflow)
{
ProcessRunner.OpenUrl($"https://openart.ai/workflows/{workflow.Id}");
}
[RelayCommand]
private async Task SearchButton()
{
DisplayedPageNumber = 1;
LatestSearchResponse = null;
searchResultsCache.Clear();
await DoSearch();
}
[RelayCommand]
private async Task OpenWorkflow(OpenArtSearchResult workflow)
{
var vm = new OpenArtWorkflowViewModel(settingsManager, packageFactory) { Workflow = workflow };
var dialog = new BetterContentDialog
{
IsPrimaryButtonEnabled = true,
IsSecondaryButtonEnabled = true,
PrimaryButtonText = Resources.Action_Import,
SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary,
IsFooterVisible = true,
MaxDialogWidth = 750,
MaxDialogHeight = 850,
CloseOnClickOutside = true,
Content = vm
};
var result = await dialog.ShowAsync();
if (result != ContentDialogResult.Primary)
return;
List<IPackageStep> steps =
[
new DownloadOpenArtWorkflowStep(openArtApi, vm.Workflow, settingsManager)
];
// Add install steps if missing nodes and preferred
if (
vm is
{
InstallRequiredNodes: true,
MissingNodes: { Count: > 0 } missingNodes,
SelectedPackage: not null,
SelectedPackagePair: not null
}
)
{
var extensionManager = vm.SelectedPackagePair.BasePackage.ExtensionManager!;
steps.AddRange(
missingNodes.Select(
extension =>
new InstallExtensionStep(
extensionManager,
vm.SelectedPackagePair.InstalledPackage,
extension
)
)
);
}
var runner = new PackageModificationRunner
{
ShowDialogOnStart = true,
ModificationCompleteTitle = Resources.Label_WorkflowImported,
ModificationCompleteMessage = Resources.Label_FinishedImportingWorkflow
};
EventManager.Instance.OnPackageInstallProgressAdded(runner);
await runner.ExecuteSteps(steps);
notificationService.Show(
Resources.Label_WorkflowImported,
Resources.Label_WorkflowImportComplete,
NotificationType.Success
);
EventManager.Instance.OnWorkflowInstalled();
}
[RelayCommand]
private void OpenOnOpenArt(OpenArtSearchResult? workflow)
{
if (workflow?.Id == null)
return;
ProcessRunner.OpenUrl($"https://openart.ai/workflows/{workflow.Id}");
}
private async Task DoSearch(int page = 0)
{
IsLoading = true;
try
{
OpenArtSearchResponse? response = null;
if (string.IsNullOrWhiteSpace(SearchQuery))
{
var request = new OpenArtFeedRequest { Sort = GetSortMode(SelectedSortMode) };
if (!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor))
{
request.Cursor = LatestSearchResponse.NextCursor;
}
response = await openArtApi.GetFeedAsync(request);
}
else
{
response = await openArtApi.SearchAsync(
new OpenArtSearchRequest
{
Keyword = SearchQuery,
PageSize = PageSize,
CurrentPage = page
}
);
}
foreach (var item in response.Items)
{
searchResultsCache.AddOrUpdate(item);
}
LatestSearchResponse = response;
}
catch (ApiException e)
{
notificationService.Show(Resources.Label_ErrorRetrievingWorkflows, e.Message);
}
finally
{
IsLoading = false;
}
}
partial void OnSelectedSortModeChanged(string? value)
{
if (value is null || SearchResults.Count == 0)
return;
searchResultsCache.Clear();
LatestSearchResponse = null;
DoSearch().SafeFireAndForget();
}
public async Task LoadNextPageAsync()
{
if (!CanGoForward)
return;
try
{
OpenArtSearchResponse? response = null;
if (string.IsNullOrWhiteSpace(SearchQuery))
{
var request = new OpenArtFeedRequest { Sort = GetSortMode(SelectedSortMode) };
if (!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor))
{
request.Cursor = LatestSearchResponse.NextCursor;
}
response = await openArtApi.GetFeedAsync(request);
}
else
{
DisplayedPageNumber++;
response = await openArtApi.SearchAsync(
new OpenArtSearchRequest
{
Keyword = SearchQuery,
PageSize = PageSize,
CurrentPage = InternalPageNumber
}
);
}
foreach (var item in response.Items)
{
searchResultsCache.AddOrUpdate(item);
}
LatestSearchResponse = response;
}
catch (ApiException e)
{
notificationService.Show("Unable to load the next page", e.Message);
}
}
private static string GetSortMode(string? sortMode)
{
return sortMode switch
{
"Trending" => "trending",
"Latest" => "latest",
"Most Downloaded" => "most_downloaded",
"Most Liked" => "most_liked",
_ => "trending"
};
}
}

188
StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs

@ -5,6 +5,7 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using AsyncImageLoader;
@ -28,8 +29,6 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
@ -52,19 +51,21 @@ public partial class OutputsPageViewModel : PageViewModelBase
private readonly INotificationService notificationService;
private readonly INavigationService<MainWindowViewModel> navigationService;
private readonly ILogger<OutputsPageViewModel> logger;
private readonly List<CancellationTokenSource> cancellationTokenSources = [];
public override string Title => Resources.Label_OutputsPageTitle;
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true };
public SourceCache<LocalImageFile, string> OutputsCache { get; } = new(file => file.AbsolutePath);
private SourceCache<PackageOutputCategory, string> categoriesCache = new(category => category.Path);
public IObservableCollection<OutputImageViewModel> Outputs { get; set; } =
new ObservableCollectionExtended<OutputImageViewModel>();
public IEnumerable<SharedOutputType> OutputTypes { get; } = Enum.GetValues<SharedOutputType>();
[ObservableProperty]
private ObservableCollection<PackageOutputCategory> categories;
public IObservableCollection<PackageOutputCategory> Categories { get; set; } =
new ObservableCollectionExtended<PackageOutputCategory>();
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanShowOutputTypes))]
@ -86,6 +87,15 @@ public partial class OutputsPageViewModel : PageViewModelBase
[ObservableProperty]
private bool isConsolidating;
[ObservableProperty]
private bool isLoading;
[ObservableProperty]
private bool showFolders;
[ObservableProperty]
private bool isChangingCategory;
public bool CanShowOutputTypes => SelectedCategory?.Name?.Equals("Shared Output Folder") ?? false;
public string NumImagesSelected =>
@ -95,6 +105,8 @@ public partial class OutputsPageViewModel : PageViewModelBase
private string[] allowedExtensions = [".png", ".webp"];
private PackageOutputCategory? lastOutputCategory;
public OutputsPageViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory,
@ -113,7 +125,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
// Observable predicate from SearchQuery changes
var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchQuery)
.Throttle(TimeSpan.FromMilliseconds(50))!
.Throttle(TimeSpan.FromMilliseconds(100))!
.Select(property => searcher.GetPredicate(property.Value))
.AsObservable();
@ -122,28 +134,37 @@ public partial class OutputsPageViewModel : PageViewModelBase
.DeferUntilLoaded()
.Filter(searchPredicate)
.Transform(file => new OutputImageViewModel(file))
.SortBy(vm => vm.ImageFile.CreatedAt, SortDirection.Descending)
.Sort(
SortExpressionComparer<OutputImageViewModel>
.Descending(vm => vm.ImageFile.CreatedAt)
.ThenByDescending(vm => vm.ImageFile.FileName)
)
.Bind(Outputs)
.WhenPropertyChanged(p => p.IsSelected)
.Throttle(TimeSpan.FromMilliseconds(50))
.Subscribe(_ =>
{
NumItemsSelected = Outputs.Count(o => o.IsSelected);
});
categoriesCache.Connect().DeferUntilLoaded().Bind(Categories).Subscribe();
settingsManager.RelayPropertyFor(
this,
vm => vm.ImageSize,
settings => settings.OutputsImageSize,
delay: TimeSpan.FromMilliseconds(250)
);
}
protected override void OnInitialLoaded()
{
RefreshCategories();
settingsManager.RelayPropertyFor(
this,
vm => vm.ShowFolders,
settings => settings.IsOutputsTreeViewEnabled,
true
);
}
public override void OnLoaded()
protected override void OnInitialLoaded()
{
if (Design.IsDesignMode)
return;
@ -151,14 +172,17 @@ public partial class OutputsPageViewModel : PageViewModelBase
if (!settingsManager.IsLibraryDirSet)
return;
base.OnLoaded();
Directory.CreateDirectory(settingsManager.ImagesDirectory);
RefreshCategories();
SelectedCategory ??= Categories.First();
SelectedOutputType ??= SharedOutputType.All;
SearchQuery = string.Empty;
ImageSize = settingsManager.Settings.OutputsImageSize;
lastOutputCategory = SelectedCategory;
IsChangingCategory = true;
var path =
CanShowOutputTypes && SelectedOutputType != SharedOutputType.All
@ -169,7 +193,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
partial void OnSelectedCategoryChanged(PackageOutputCategory? oldValue, PackageOutputCategory? newValue)
{
if (oldValue == newValue || newValue == null)
if (oldValue == newValue || oldValue == null || newValue == null)
return;
var path =
@ -177,11 +201,12 @@ public partial class OutputsPageViewModel : PageViewModelBase
? Path.Combine(newValue.Path, SelectedOutputType.ToString())
: SelectedCategory.Path;
GetOutputs(path);
lastOutputCategory = newValue;
}
partial void OnSelectedOutputTypeChanged(SharedOutputType? oldValue, SharedOutputType? newValue)
{
if (oldValue == newValue || newValue == null)
if (oldValue == newValue || oldValue == null || newValue == null)
return;
var path =
@ -262,8 +287,13 @@ public partial class OutputsPageViewModel : PageViewModelBase
public void Refresh()
{
Dispatcher.UIThread.Post(() => RefreshCategories());
Dispatcher.UIThread.Post(OnLoaded);
Dispatcher.UIThread.Post(RefreshCategories);
var path =
CanShowOutputTypes && SelectedOutputType != SharedOutputType.All
? Path.Combine(SelectedCategory.Path, SelectedOutputType.ToString())
: SelectedCategory.Path;
GetOutputs(path);
}
public async Task DeleteImage(OutputImageViewModel? item)
@ -273,8 +303,8 @@ public partial class OutputsPageViewModel : PageViewModelBase
var confirmationDialog = new BetterContentDialog
{
Title = "Are you sure you want to delete this image?",
Content = "This action cannot be undone.",
Title = Resources.Label_AreYouSure,
Content = Resources.Label_ActionCannotBeUndone,
PrimaryButtonText = Resources.Action_Delete,
SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary,
@ -352,8 +382,8 @@ public partial class OutputsPageViewModel : PageViewModelBase
{
var confirmationDialog = new BetterContentDialog
{
Title = $"Are you sure you want to delete {NumItemsSelected} images?",
Content = "This action cannot be undone.",
Title = string.Format(Resources.Label_AreYouSureDeleteImages, NumItemsSelected),
Content = Resources.Label_ActionCannotBeUndone,
PrimaryButtonText = Resources.Action_Delete,
SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary,
@ -365,12 +395,13 @@ public partial class OutputsPageViewModel : PageViewModelBase
var selected = Outputs.Where(o => o.IsSelected).ToList();
Debug.Assert(selected.Count == NumItemsSelected);
var imagesToRemove = new List<LocalImageFile>();
foreach (var output in selected)
{
// Delete the file
var imageFile = new FilePath(output.ImageFile.AbsolutePath);
var result = await notificationService.TryAsync(imageFile.DeleteAsync());
if (!result.IsSuccessful)
{
continue;
@ -383,15 +414,10 @@ public partial class OutputsPageViewModel : PageViewModelBase
await notificationService.TryAsync(sideCar.DeleteAsync());
}
OutputsCache.Remove(output.ImageFile);
// Invalidate cache
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
{
loader.RemoveAllNamesFromCache(imageFile.Name);
}
imagesToRemove.Add(output.ImageFile);
}
OutputsCache.Remove(imagesToRemove);
NumItemsSelected = 0;
ClearSelection();
}
@ -491,7 +517,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
}
}
OnLoaded();
Refresh();
IsConsolidating = false;
}
@ -517,20 +543,58 @@ public partial class OutputsPageViewModel : PageViewModelBase
return;
}
var files = Directory
.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories)
.Where(path => allowedExtensions.Contains(new FilePath(path).Extension))
.Select(file => LocalImageFile.FromPath(file))
.ToList();
if (files.Count == 0)
if (lastOutputCategory?.Path.Equals(directory) is not true)
{
OutputsCache.Clear();
IsChangingCategory = true;
}
else
IsLoading = true;
cancellationTokenSources.ForEach(cts => cts.Cancel());
Task.Run(() =>
{
OutputsCache.EditDiff(files);
}
var getOutputsTokenSource = new CancellationTokenSource();
cancellationTokenSources.Add(getOutputsTokenSource);
if (getOutputsTokenSource.IsCancellationRequested)
{
cancellationTokenSources.Remove(getOutputsTokenSource);
return;
}
var files = Directory
.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories)
.Where(file => allowedExtensions.Contains(new FilePath(file).Extension))
.Select(file => LocalImageFile.FromPath(file))
.ToList();
if (getOutputsTokenSource.IsCancellationRequested)
{
cancellationTokenSources.Remove(getOutputsTokenSource);
return;
}
Dispatcher.UIThread.Post(() =>
{
if (files.Count == 0 && OutputsCache.Count == 0)
{
IsLoading = false;
IsChangingCategory = false;
return;
}
OutputsCache.EditDiff(
files,
(oldItem, newItem) => oldItem.AbsolutePath == newItem.AbsolutePath
);
IsLoading = false;
IsChangingCategory = false;
});
cancellationTokenSources.Remove(getOutputsTokenSource);
});
}
private void RefreshCategories()
@ -559,7 +623,10 @@ public partial class OutputsPageViewModel : PageViewModelBase
pair.InstalledPackage.FullPath!,
pair.BasePackage.OutputFolderName
),
Name = pair.InstalledPackage.DisplayName ?? ""
Name = pair.InstalledPackage.DisplayName ?? "",
SubDirectories = GetSubfolders(
Path.Combine(pair.InstalledPackage.FullPath!, pair.BasePackage.OutputFolderName)
)
}
)
.ToList();
@ -569,18 +636,37 @@ public partial class OutputsPageViewModel : PageViewModelBase
new PackageOutputCategory
{
Path = settingsManager.ImagesDirectory,
Name = "Shared Output Folder"
Name = "Shared Output Folder",
SubDirectories = GetSubfolders(settingsManager.ImagesDirectory)
}
);
packageCategories.Insert(
1,
new PackageOutputCategory { Path = settingsManager.ImagesInferenceDirectory, Name = "Inference" }
);
categoriesCache.EditDiff(packageCategories, (a, b) => a.Path == b.Path);
SelectedCategory = previouslySelectedCategory ?? Categories.First();
}
private ObservableCollection<PackageOutputCategory> GetSubfolders(string strPath)
{
var subfolders = new ObservableCollection<PackageOutputCategory>();
if (!Directory.Exists(strPath))
return subfolders;
var directories = Directory.EnumerateDirectories(strPath, "*", SearchOption.TopDirectoryOnly);
foreach (var dir in directories)
{
var category = new PackageOutputCategory { Name = Path.GetFileName(dir), Path = dir };
Categories = new ObservableCollection<PackageOutputCategory>(packageCategories);
if (Directory.GetDirectories(dir, "*", SearchOption.TopDirectoryOnly).Length > 0)
{
category.SubDirectories = GetSubfolders(dir);
}
subfolders.Add(category);
}
SelectedCategory =
Categories.FirstOrDefault(x => x.Name == previouslySelectedCategory?.Name) ?? Categories.First();
return subfolders;
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save