Compare commits

..

987 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 8 months ago
JT e5d215463e updated spanish/french/turkish translations 8 months ago
JT 3d1090573b
Merge pull request #588 from ionite34/fix-model-images 8 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 18190dced1
Merge pull request #512 from LykosAI/main 8 months ago
JT f61b2255d2
Merge branch 'LykosAI:main' into main 8 months ago
JT b8ad0a7b01 Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 8 months ago
JT a09775a207
Merge pull request #557 from ionite34/backport/main/pr-556 8 months ago
JT 49a88ff774
Merge branch 'main' into backport/main/pr-556 8 months ago
JT bbb35f7eaf
Merge pull request #558 from ionite34/add-strings 8 months ago
JT bc04756169 Add missing strings from dev & update chagenlog 8 months ago
JT 56b6a2c279 Really confirm package deletion 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 005826f276
Merge pull request #554 from ionite34/downgrade-icons 8 months ago
JT 645d91055a Fix dark icons on dark theme on macOS 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
JT a0ded63b5e
Merge pull request #552 from ionite34/issue-fixes 8 months ago
JT f8f3cd09b4 update chagenlog 8 months ago
JT 6cc3596c56 fix tests & add pony 8 months ago
JT 66fa815d6b Fix a few issues from github & placeholder text when deleting models 8 months ago
JT c6fc37efaa Merge branch 'main' of https://github.com/ionite34/StabilityMatrix 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 5a53afa766
Merge pull request #495 from LykosAI/main 9 months ago
Ionite 80585f8640
Merge branch 'main' into main 9 months ago
Ionite ac2b9a86d6
Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 9 months ago
Ionite 6f1f2662c7
Merge pull request #546 from ionite34/merge-dev-to-main-298e577 9 months ago
Ionite 298e577a72
Merge pull request #545 from ionite34/changelog 9 months ago
Ionite d11dc9e69d
Update CHANGELOG.md 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
Ionite 5b378a381e
Update CHANGELOG.md 9 months ago
Ionite b73f0de1d9
Version bump 9 months ago
Ionite 536818e7e9
Consolidate v2.9.0 changelog 9 months ago
Ionite 9f3a891bdc
Merge pull request #544 from ionite34/update-translations 9 months ago
Ionite a8af39780a
Merge branch 'dev' into update-translations 9 months ago
Ionite 4fdf4159d9
Merge pull request #542 from ionite34/fix-unknown-checkpoint-display 9 months ago
JT a47ee7eb96
chagenlog 9 months ago
JT 54102b2ea2 Updated translations from poeditor 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
JT 961cf365e9
Merge branch 'dev' into fix-unknown-checkpoint-display 9 months ago
Ionite 08e1034abf
Merge pull request #543 from ionite34/fix-image-load 9 months ago
Ionite 2862e90622
Update CHANGELOG.md 9 months ago
Ionite 37f1ac512c
Add error handling for SelectImage card hashing 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
JT cd15b76505
chagenlog 9 months ago
JT 54b1e9a216
Fix unknown model types not showing on checkpoints page 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
JT a17e4f202a
Merge pull request #540 from ionite34/moar-models 9 months ago
JT ff84b469ca Merge branch 'moar-models' of https://github.com/ionite34/StabilityMatrix into moar-models 9 months ago
JT f27671bc9d name 9 months ago
JT 51c378b363
Merge branch 'dev' into moar-models 9 months ago
JT 477d5302dd moar options in all the model browsers 9 months ago
JT 7e63065df5
Merge pull request #539 from ionite34/fix-civitai-login 9 months ago
JT bce305b216 Fix civitai login dialog not showing when needed 9 months ago
Ionite dc4e99b489
Merge branch 'dev' into openart 9 months ago
JT 60a5ae4da7
Merge pull request #538 from ionite34/launch-package 9 months ago
JT fb3257ed4f added xmldoc 9 months ago
JT 50b7c2cdff added `--launch-package` argument for launching a package on startup 9 months ago
JT 53762b678f
Merge pull request #537 from ionite34/fix-image-nav 9 months ago
JT 736ea79292 add System.Drawing.Common 9 months ago
JT 9d78a45cd8 Fix ImageViewerDialog arrow key nav & update nugets 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
Ionite 27837e82a4
Merge pull request #536 from ionite34/ionite34-patch-1 9 months ago
Ionite 59fecaeb2e
Update CHANGELOG.md 9 months ago
Ionite 75cdf9b673
Merge pull request #531 from ionite34/prompt-expansion 9 months ago
Ionite 64a00cb1c4
Update CHANGELOG.md 9 months ago
Ionite 34916633b6
Fix invalid thread error in package restart 9 months ago
Ionite d8587466d0
Merge branch 'dev' into prompt-expansion 9 months ago
JT fb8f06d611 nav headers! and buttons! 9 months ago
Ionite fe1cf2f423
Merge pull request #535 from ionite34/fix-image-copy 9 months ago
Ionite 32fa8d6607
Update CHANGELOG.md 9 months ago
Ionite 97c1d15e20
Add file copy mode for non windows 9 months ago
Ionite 56455ace1b
Update CHANGELOG.md 9 months ago
Ionite 7c746138e3
Move dynamic name binding to static instead 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 6a876045d7
Merge pull request #533 from ionite34/maybe-fix-a1111-update 9 months ago
JT 6a66f3e1b7
chagenlog 9 months ago
JT c9a9c4c805
Fix SD Forge update also 9 months ago
JT bb7ad63817
Use venvRunner.Setup instead of SetupVenv during install/update 9 months ago
JT bc98c56dcd
Merge pull request #532 from ionite34/fix-forge-outputs 9 months ago
JT 2c38289386
Update CHANGELOG.md 9 months ago
JT af76365153 Fix output paths for Forge 9 months ago
Ionite 872c3b0c0e
Remove unused imports 9 months ago
Ionite 4e3e07205c
Made LaunchAsync public for api restarting 9 months ago
Ionite 2eb631d504
Add package restart when installing extensions 9 months ago
Ionite b0cec3d3ea
Merge branch 'dev' into prompt-expansion 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
Ionite c1aca685f8
Create DownloadDirectory before starting download 9 months ago
Ionite 74605f6513
oops 9 months ago
Ionite c968846d87
Add PromptCard modules card state saving 9 months ago
Ionite 0e65abe103
Add OneOf dependency 9 months ago
Ionite 49398c3386
Add Extension prompt checking 9 months ago
Ionite 80f28815d3
Add ExtensionManifest implicit from string 9 months ago
Ionite c34f608912
Add PromptExpansion node, change clip to accept OneOf 9 months ago
Ionite 20e28d0443
Remove negative prompt expansion 9 months ago
Ionite 5025822d02
Add node connection normalizing for IOneOf 9 months ago
Ionite d12e04a69b
Add NodeConnectionBase Json converter 9 months ago
Ionite 833950c349
Add PromptExpansion model sharing 9 months ago
Ionite d7aa73a260
Add OneOfJsonConverter for ComfyClient 9 months ago
Ionite 942013673a
Add ApiFactory option for custom refit settings 9 months ago
Ionite a6f09bc4b4
Add GetManifestExtensionsMapAsync 9 months ago
Ionite e241a15873
Add PromptExpansionCard missing StyleInclude 9 months ago
Ionite ffb6da8d18
Remove redundant attribute ctors 9 months ago
Ionite c215b0c73a
Add IComfyStep support for StackEditableCardViewModel 9 months ago
Ionite 2d22db1374
Implement Prompt Expansion module step 9 months ago
Ionite d0c650051a
Add PromptCard style steps 9 months ago
Ionite a119dde5b0
Add ClassTypeRequiredExtensions store for NodeDictionary 9 months ago
Ionite 2a8088fd4a
Add GetInstalledExtensionsLiteAsync for faster local parsing 9 months ago
Ionite ebf6ad47f6
Add ComfyClient.LocalServerPackage 9 months ago
Ionite 27dd1576cb
Localizable false for node names 9 months ago
Ionite 4c3897efa4
Use .NET 8 class statements 9 months ago
Ionite 44a289565d
Docstrings 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 4ce285a849
Merge pull request #530 from ionite34/mac-fixes 9 months ago
jt bb465b502d fixed invoke install on macos & fixed forge cli args on macos 9 months ago
Ionite 31f059f12e
Remove NodeConnectionBase implicit array conversion 9 months ago
Ionite fceb661063
Add TypedNodeOptions attribute for naming 9 months ago
Ionite 19383fde5a
Add prompt expansion module 9 months ago
JT 5e7b9286ac just put the whole json in there (for now) 9 months ago
JT d4abd58d75
Merge pull request #529 from ionite34/downmerge 9 months ago
JT 254f79e5bb
Merge branch 'dev' into downmerge 9 months ago
JT 57a0597296 Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into openart 9 months ago
Ionite 39a7023946
Merge pull request #528 from ionite34/download-archives 9 months ago
Ionite 9a05a787b9
Remove unused DI fields 9 months ago
Ionite c3022320bf
Add TreeFileExplorer control 9 months ago
Ionite e8b0c0902f
Simplify ImageFolderCard search box 9 months ago
Ionite fe11c8d50b
Add tracked download extract support 9 months ago
Ionite 0d81845b95
Add File and Folder browser IOCommands 9 months ago
Ionite a2bff10a1b
Simplify ControlNet and Upscaler download usages 9 months ago
Ionite 7e80d9847d
Auto extract for TrackedDownload and RemoteResource 9 months ago
JT 1e6049581e
Merge pull request #527 from ionite34/fix-swarm-macos 9 months ago
JT 439f59f311
Update CHANGELOG.md 9 months ago
jt 244bca04b0 Use Path.DirectorySeparatorChar instead of backslashes 9 months ago
Ionite 69a2c08b5d
Merge pull request #463 from LykosAI/main 9 months ago
Ionite c495f657e6
Merge branch 'main' into main 9 months ago
Ionite d5bdfc5355
Merge pull request #526 from ionite34/fix-stack-card-icon 9 months ago
Ionite efd5614799
Update CHANGELOG.md 9 months ago
Ionite df4d69a9ed
Fix StackEditableCard icon sizes 9 months ago
Ionite 24a448d2a3
Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 9 months ago
Ionite 6592198b4d
Merge pull request #525 from ionite34/backport/main/pr-524 9 months ago
Ionite 32d46c5cf5 Update CHANGELOG.md 9 months ago
Ionite 6919e6841a Update Fooocus `--output-directory` argument to `--output-path` 9 months ago
Ionite 2e05f62921
Merge pull request #524 from ionite34/fix-fooocus-arg 9 months ago
Ionite ac7c599e07
Update CHANGELOG.md 9 months ago
Ionite ce28b9a2d2
Update Fooocus `--output-directory` argument to `--output-path` 9 months ago
JT 889877724b
Merge branch 'dev' into openart 9 months ago
Ionite eb918f1f54
Merge pull request #522 from ionite34/backport/main/pr-521 9 months ago
Ionite 4bbc0c8275 Update CHANGELOG.md 9 months ago
Ionite a3d95abf2d Add back some stuff removed in 81652930 9 months ago
Ionite b5e5ccf38c
Merge pull request #521 from ionite34/fix-multi-image 9 months ago
Ionite b7485490d3
Update CHANGELOG.md 9 months ago
Ionite 00e0bfc3c3
Add back some stuff removed in 81652930 9 months ago
Ionite 188be7bb45
Merge pull request #519 from ionite34/model-configs 9 months ago
Ionite 07e3d05f27
Update CHANGELOG.md 9 months ago
Ionite 993eb15e8d
Add Config help dialog in menu 9 months ago
JT 9f430ecb65 settings n bug fixes 9 months ago
Ionite 95c8e9e8d1
Fix equality comparers for HybridModelFile 9 months ago
Ionite cb00e6a4c4
Remove unused comment 9 months ago
Ionite f8e3d80bbc
Add CheckpointLoader with config 9 months ago
Ionite 48fc6d5139
Add ComfyClient UploadFilesAsync, LocalServerPath 9 months ago
JT e87b5032be rearrange settings & add System.Drawing.Common reference? 9 months ago
Ionite a3829d6a68
Add config loading for Inference 9 months ago
JT c26960ce1f added basic card UI, searching, and infiniscroll 9 months ago
Ionite a62038d19c
Add System.IO.Hashing dependency 9 months ago
Ionite dace196657
Add some localizable false 9 months ago
Ionite b91a24e451
Merge branch 'model-configs' of https://github.com/ionite34/StabilityMatrix into model-configs 9 months ago
Ionite f2ac6abb30
Remove unused StepIndex and StepTypeIndex 9 months ago
Ionite 113df1968e
Merge branch 'dev' into model-configs 9 months ago
JT b4af5906fd Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into openart 9 months ago
JT b88784f069
Merge pull request #518 from ionite34/choose-download-location 9 months ago
JT 5a41985c04 show error if no custom path 9 months ago
JT 2998ceee21 Added models & refit interface for openart stuff 9 months ago
Ionite a18014db76
Add icon for models with config 9 months ago
Ionite 19f4663763
Add indexing of LocalModelFile.ConfigFullPath 9 months ago
JT 6c7c6ead56 Added the ability to choose where CivitAI model downloads are saved 9 months ago
Ionite 201ff487d3
Merge pull request #517 from ionite34/backport/main/pr-516 9 months ago
Ionite 0aa5a49cce
Merge pull request #516 from ionite34/fix-secrets-manager 9 months ago
Ionite 55dc469425
Merge pull request #516 from ionite34/fix-secrets-manager 9 months ago
Ionite f822f4d79f
Update CHANGELOG.md 9 months ago
Ionite 71a6c463a8
Add Warn logging for failed TrackedDownload 9 months ago
Ionite 939ca5308d
Use SafeLoad for secrets to prevent errors stop 9 months ago
Ionite 9c00cb6373
Make secrets decode error level 9 months ago
JT 89f0545590
Merge pull request #513 from ionite34/forge 9 months ago
Ionite a0ff4a07dc
Merge pull request #447 from LykosAI/main 9 months ago
JT c477e065e6 Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 9 months ago
JT f2f8e678cd
Merge branch 'dev' into forge 9 months ago
Ionite a4234836c5
Merge pull request #515 from ionite34/downmerge 9 months ago
JT a4040d16a1
Merge branch 'dev' into downmerge 9 months ago
JT 269a9127ed
Merge pull request #514 from ionite34/fix-extension-dialog 9 months ago
JT 59af8bc07d Fixed extension modification dialog not showing any progress messages & added GFPGAN link to a1111 9 months ago
JT 4f3c4718f7 chagenlog 9 months ago
JT a0d6ae12f4 Added SDWebUi Forge by lllyasviel & fixed extension modification not showing any progress. also enabled extensions for vlad & ui-ux 9 months ago
Ionite dfc27fc05d
Merge pull request #512 from ionite34/backport/main/pr-511 9 months ago
JT 587fee5b77
Merge pull request #511 from ionite34/recommended-fix 9 months ago
Ionite 735a2aa225
Merge pull request #510 from ionite34/backport/main/pr-509 9 months ago
JT 5915b131ed
Merge pull request #511 from ionite34/recommended-fix 9 months ago
JT 842950b6e3
Merge branch 'dev' into recommended-fix 9 months ago
JT 112cc652de Fix crash on recommended models dialog when civitai ded 9 months ago
Ionite 2691fd2462
Merge pull request #509 from ionite34/inference-fixes 9 months ago
Ionite 43869300ee
Merge pull request #509 from ionite34/inference-fixes 9 months ago
Ionite 120526d950
Update CHANGELOG.md 9 months ago
Ionite cd178b6f5d
Merge branch 'dev' into inference-fixes 9 months ago
Ionite ed61fa66b8
Fix backport action 9 months ago
Ionite 3b7653488a
Merge pull request #508 from ionite34/backport/main/pr-507 9 months ago
ionite34 c6ae44cc8e
Update project version 9 months ago
ionite34 a26de6b9f2
Update CHANGELOG.md 9 months ago
ionite34 027d846216
Move SDTurbo Denoise strength to temp property 9 months ago
Ionite 14c47a7175
Merge pull request #507 from ionite34/fix-inference-denoise 9 months ago
Ionite 604fe366b3
Fix addon conditioning not applied in sampler 9 months ago
Ionite 8c97a4f792
Fix downloadable controlnet using upscalers source 9 months ago
ionite34 b9a5cb3786
Update project version 9 months ago
ionite34 d7a03edc32
Update CHANGELOG.md 9 months ago
ionite34 1b4cadf5da
Move SDTurbo Denoise strength to temp property 9 months ago
JT 6007de8b7c
Merge pull request #488 from ionite34/stable-swarm 9 months ago
Ionite c6a4af1fc1
Merge pull request #439 from LykosAI/main 9 months ago
JT d9a7ec3489 Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into stable-swarm 9 months ago
JT e0b2d6b308
Merge branch 'dev' into stable-swarm 9 months ago
Ionite edb81c106b
Merge branch 'main' into main 9 months ago
JT 114cc12d24 Merge branch 'stable-swarm' of https://github.com/ionite34/StabilityMatrix into stable-swarm 9 months ago
JT 9a72a910cc dont show in one-click for now 9 months ago
Ionite 9bd67b1e39
Merge pull request #506 from ionite34/merge-dev-to-main-5c0c582 9 months ago
Ionite ab4fc7633a
Merge branch 'main' into merge-dev-to-main-5c0c582 9 months ago
Ionite 5c0c582d0c
Merge pull request #505 from ionite34/fix-index-checkpoints 9 months ago
Ionite 14f1ffd31f
Error handling for index checkpoints 9 months ago
Ionite 909a2903fb
Merge pull request #438 from LykosAI/main 10 months ago
JT ea27537ff9
Merge pull request #504 from ionite34/release-2.8.1 10 months ago
JT d309c24a02
Merge branch 'dev' into stable-swarm 10 months ago
Ionite fb122d9c7d
Merge pull request #503 from ionite34/fix-update-appstart 10 months ago
ionite34 2e4989c521
Add app name to debug info 10 months ago
ionite34 3ea66c2bdf
Update CHANGELOG.md 10 months ago
ionite34 ad19b620c8
Fix StartApp with args on macos 10 months ago
Ionite 7995b90327
Merge pull request #502 from ionite34/update-readme 10 months ago
ionite34 0c805f3c0d
Update README packages and platform support 10 months ago
Ionite a67131f0b9
Merge pull request #501 from ionite34/process-fixes 10 months ago
Ionite 0d506bf45a
Merge branch 'dev' into process-fixes 10 months ago
ionite34 4cc5e43e33
Add backport action 10 months ago
JT b7aa787b3f
Merge branch 'dev' into stable-swarm 10 months ago
JT 3ca62b172a
Merge pull request #500 from ionite34/fix-onetrainer 10 months ago
ionite34 23c0dea443
Update CHANGELOG.md 10 months ago
ionite34 468b5743a4
Include output in Extract7z process errors 10 months ago
ionite34 3a0f5b884d
Move PortableGitDownloadUrl to const 10 months ago
ionite34 a91bbb9319
Throw helpers for ProcessException 10 months ago
JT 2b51c5f49e added comfy install prompt for stable swarm & remaining pieces for swarm I think? launch args, symlink/config settings, etc... 10 months ago
JT 90cb25ccbb Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into stable-swarm 10 months ago
JT e6c329aafe remove github client 10 months ago
JT 19ec7e0612 Add Tkinter prerequisite and handle it 10 months ago
Ionite 8c8a167bb0
Merge pull request #499 from ionite34/fix-settings 10 months ago
Ionite 60d5357572
Update CHANGELOG.md 10 months ago
Ionite e56b9d0c74
Fix SettingsManager multithread locks 10 months ago
JT 5b816adbe8
Merge pull request #498 from ionite34/downmerge 10 months ago
JT d712eefc5a
Merge branch 'dev' into downmerge 10 months ago
JT 79afd1454c
Merge pull request #497 from ionite34/fix-ruined-fooocus 10 months ago
JT c663482177 moar fixes & chagenlog 10 months ago
JT 598ee6ef45 chagenlog 10 months ago
JT c69f38dacb dont use config cuz I guess ruined fork hasnt picked up that change yet 10 months ago
Ionite be5ca65dcd
Merge pull request #430 from slogonomo/patch-1 10 months ago
slogonomo a11eaf2afb
Update UnixPrerequisiteHelper.cs 10 months ago
JT f0dfa05d27
Merge pull request #427 from LykosAI/main 10 months ago
JT bb5ff4a8d7
Merge branch 'LykosAI:main' into main 10 months ago
JT ae6d90b76b Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 10 months ago
JT 8cbe030338
Merge pull request #495 from ionite34/release-2.8.0 10 months ago
Ionite 86286b9127
Merge pull request #496 from ionite34/oops 10 months ago
Ionite 5a6a20dab7
Merge branch 'dev' into oops 10 months ago
Ionite 96f55c537c
Delete .backportrc.json 10 months ago
JT 16d9bf5bc6
Merge pull request #494 from ionite34/release-chagenlog 10 months ago
JT 7194657b35 remove duplicate french update 10 months ago
JT 086d79835e fix extra newline 10 months ago
JT 0c6a74b96e update chagenlog for release 10 months ago
JT d14bd651b5
Merge pull request #493 from ionite34/fix-search-by-id-sort 10 months ago
JT 9330cc98ed change property too 10 months ago
JT 2a132d4f16 Fix sorting when searching by model ID in CivitAiBrowser 10 months ago
Ionite 2c2b3875b0
Merge pull request #492 from ionite34/fooocus-config 10 months ago
Ionite f06128d299
oops stream not closed 10 months ago
Ionite 31981760e0
chagenlog 10 months ago
Ionite a6e5bd623c
dont write default in None mode 10 months ago
Ionite 0b99879db6
Preserve unknown keys in fooocus config 10 months ago
Ionite 256f6ef394
Merge pull request #491 from ionite34/fixes 10 months ago
Ionite 0f5ffda5ce
Merge branch 'dev' into fixes 10 months ago
Ionite 7c752890b3
Maybe fix tests 10 months ago
Ionite 5db1e6e428
Fix tests on linux 10 months ago
Ionite daf6071afc
Add FileSystemPath Tests 10 months ago
Ionite e19f18d0d4
Fix ambiguous equality operators 10 months ago
JT 2c495730c6
Merge branch 'dev' into stable-swarm 10 months ago
JT dd87c66c00
Merge pull request #490 from ionite34/moar-fix 10 months ago
JT e736708e74 added model check to ImageToVideo also 10 months ago
Ionite 3d5a4aeea3
Cleanup unused 10 months ago
Ionite 76cff0b5cd
Remove legacy PrerequisiteHelper 10 months ago
Ionite 1decfaeae4
More settings cleanup unused funcs 10 months ago
Ionite c2fb73a5e5
Cleanup some redundant settingsmanager funcs 10 months ago
Ionite 633a61863e
Primary ctor for SettingsTransaction 10 months ago
Ionite 5c2a3087db
Refactors for LibraryDir type change 10 months ago
Ionite dc16dfb76b
Change LibraryDir to DirectoryPath type 10 months ago
Ionite 5c4101d364
Cleanup 10 months ago
Ionite afeef2d97f
Add IPathObject.Info 10 months ago
Ionite ccdf6c9d32
Equality improvements for FileSystemPath 10 months ago
JT 810022ccf3 chagenlog 10 months ago
JT d3f3c858a0 fixed a few bugs from sentry, see chagenlog for details 10 months ago
JT c39f560ed8
Merge branch 'dev' into stable-swarm 10 months ago
Ionite 145a8da9f1
Merge pull request #489 from ionite34/controlnet-fix 10 months ago
Ionite edd4460981
Fix controlnet missing image error 10 months ago
JT 7f0325f086 wip beginnings of stable swarm. should install & launch but not everything hooked up yet 10 months ago
Ionite 7f1664c519
Merge pull request #480 from ionite34/notifications 10 months ago
Ionite d01651e833
Add chagenlog 10 months ago
Ionite 783934a447
Fix outputs not containing grid for notifications 10 months ago
Ionite 2d2a54c644
Fix notification init on macos 10 months ago
Ionite 39ed0084e9
Merge branch 'dev' into notifications 10 months ago
JT 13ae4d7330
Merge pull request #487 from ionite34/package-prereqs 10 months ago
JT 01c7847484
Merge branch 'dev' into package-prereqs 10 months ago
JT 585b4771da each package now defines its own prerequisites and each install now calls InstallPackageRequirements instead of InstallAllIfNecessary 10 months ago
Ionite dd1f3d2a14
Include install location in notification 10 months ago
Ionite 3645ff4102
Remove duplicate notification 10 months ago
Ionite 6d8fb3258f
Add notifications for package install 10 months ago
Ionite 97e7f6fb9a
Add notification for package updates 10 months ago
Ionite 91a9bb8eee
Fix unobserved task 10 months ago
Ionite ba195d259a
Add notification for one click install 10 months ago
Ionite 6a151a5c2f
Add NotificationServiceExtensions for package completion handling 10 months ago
Ionite aeb63527f7
Completion events and progress messages for PackageModificationRunner 10 months ago
Ionite 653782ef5a
More notification keys 10 months ago
Ionite 2d12d1cbe8
Merge branch 'dev' into notifications 10 months ago
JT b7f0f0d680
Merge pull request #486 from ionite34/moar-chagenlog 10 months ago
JT 33edda5778
Merge branch 'dev' into moar-chagenlog 10 months ago
JT 12f904267a chagenlog 10 months ago
JT 172b2a1a8c
Merge pull request #485 from ionite34/fix-outputs-and-chagenlog 10 months ago
JT 2088d3c0aa Fix outputs page crash & update chagenlog 10 months ago
JT a45949fdbb Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into show-package-install 10 months ago
JT 3003e05e7d
Merge pull request #483 from ionite34/show-package-install 10 months ago
JT 47b15f4808
Merge branch 'dev' into show-package-install 10 months ago
JT 72c8f264d7 Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into show-package-install 10 months ago
JT da7d3830cc
Merge pull request #484 from ionite34/mac-fixes 10 months ago
jt 621073c7c3 updated a1111 and ui/ux packages to work on mac & maybe fixed invoke install on mac 10 months ago
JT 94e0c2d7ba Merge branch 'show-package-install' of https://github.com/ionite34/StabilityMatrix into show-package-install 10 months ago
JT e87a63f9d1 poof 10 months ago
JT 929d5e0298
Merge branch 'dev' into show-package-install 10 months ago
JT b656d4c3d1 undo inference revert 10 months ago
JT c85bf77fcb Show package install after RecommendedModelsDialog is closed 10 months ago
Ionite a1bab3a03c
Merge branch 'dev' into notifications 10 months ago
JT 3ad56e66ee
Merge pull request #482 from ionite34/buncha-fixes 10 months ago
JT 476756aab0 Wrap install in a task and fire&forget so that we don't click "install" and just sit there waiting for a second before the dialog disappears 10 months ago
JT 68aec6a218
Merge pull request #481 from ionite34/buncha-fixes 10 months ago
JT 83994c9384 refactored non-blocking post-install stuff & added isInferenceInstall to new one click 10 months ago
JT d157418327
Merge branch 'dev' into buncha-fixes 10 months ago
JT 1a8fd000f5 Updated torch version for A1111, fixed vlad update maybe, updated Fooocus launch args, and fixed OneClickInstallFinished event not firing 10 months ago
JT 4831e1fe58
Merge pull request #479 from ionite34/new-one-click 10 months ago
JT d18b3a30fb pls review 10 months ago
JT e2133c50ba chagenlog 10 months ago
JT 0a513ff25c added skip one-click button 10 months ago
JT 490745f77c mostly finished recommended models view 10 months ago
JT c39e9b6c75 recommended models dialog 10 months ago
Ionite 78b150c1c2
Add prompt completion notification 10 months ago
Ionite 1ce19c8493
Add debug show notification 10 months ago
Ionite cf7636bb19
Made GetNativeNotificationsManagerAsync public 10 months ago
Ionite 05a4c999bd
Switch to primary ctor 10 months ago
Ionite df22b23916
Attach handlers before invoke 10 months ago
Ionite 9954647020
UI update 10 months ago
Ionite f6a2fecce0
Add download notifications 10 months ago
Ionite e21c1bb0c4
Fix targetframework for notifications 10 months ago
Ionite cc3fa54dac
Use parsable serializer 10 months ago
Ionite 893951c46a
Switch to non weak events, fix downloads GC too early 10 months ago
Ionite 40028cec6a
ProgressStateChanging event 10 months ago
Ionite 3e02f223b9
Add ProgressStateChanging event 10 months ago
Ionite f380a7676e
Fix property json converter 10 months ago
Ionite e68b91e3d6
Fix sorting value 10 months ago
Ionite 59e0df2773
NotificationKey parsing and levels 10 months ago
Ionite 6828245e94
Improved converters 10 months ago
Ionite 928c283625
Add some spacing 10 months ago
Ionite 0a540a8f39
Update NotificationKey.cs 10 months ago
Ionite 94e36ed683
NotificationService native notification support 10 months ago
Ionite 8a4eaae59f
Add notifications settings subpage 10 months ago
Ionite a35c1f37a9
Add StringValue GetValues helper 10 months ago
Ionite 23a66f4ceb
Fix BetterComboBox property accessibility 10 months ago
Ionite 625e7b279c
Add EnumAttributeConverters 10 months ago
JT abe631df13 added non-blocking package runner option to show new dialog that doesnt exist yet 10 months ago
JT e2b355e443 new one click installer dialog (wip) 10 months ago
JT 862951a4d6
Merge pull request #478 from ionite34/create-outputs-dir 10 months ago
JT 2b037f44c7 Create Fooocus outputs dir on first launch 10 months ago
Ionite 6b0d5b726b
Merge pull request #477 from ionite34/extensions-req 10 months ago
Ionite d864004d26
poof the trim 10 months ago
Ionite fb15ba7110
Update CHANGELOG.md 10 months ago
Ionite 51b6d0c9b3
Skip requirements.txt if empty 10 months ago
JT d7ed54edf1
Merge pull request #476 from ionite34/fix-crash 10 months ago
JT caa3a6638b
Merge branch 'dev' into fix-crash 10 months ago
JT 15a09d2f81 Fix crash when invalid launch args passed & change Close button to Hide since that's what it does 10 months ago
Ionite d0ecac15b7
Merge pull request #475 from ionite34/exception-copy 10 months ago
Ionite 6a78e5d752
Update CHANGELOG.md 10 months ago
Ionite 47736c828c
Add copy button for exception dialog 10 months ago
Ionite 3827c189c4
Add CopyDetails resource 10 months ago
JT 362d2b9e9a
Merge pull request #474 from ionite34/fooocus-config-and-nugets 10 months ago
JT f2997610d6 Added Config mode for Fooocus shared models & updated nuget packages 10 months ago
Ionite 436003d183
Merge pull request #473 from ionite34/fix-webp 10 months ago
Ionite 0b3ec589e5
Update CHANGELOG.md 10 months ago
Ionite 3ca0f3670d
Fix ImageViewerDialog arrow key navigation focus 10 months ago
Ionite 28fdd6c235
Use HttpClientFactory 10 months ago
Ionite 7d437a55e5
Update CHANGELOG.md 10 months ago
Ionite bd8d4885dd
Add animated imagesource template parsing 10 months ago
Ionite 0c11f30f05
Add WebpReader 10 months ago
JT 530a79bc1d
Merge pull request #405 from LykosAI/main 10 months ago
JT 12d4b126c8
Merge branch 'LykosAI:main' into main 10 months ago
JT ed707d612c
Merge pull request #472 from ionite34/readme 10 months ago
JT b78dbc961f
Merge branch 'dev' into readme 10 months ago
JT e986af1803 readme language update 10 months ago
JT 5e993c721a
Merge pull request #471 from ionite34/german-and-portuguese 10 months ago
JT c80c01f3c5 Added german & portuguese & updated some other languages. also made sidebar width dynamic based on language 10 months ago
Ionite 457c326c6e
Merge pull request #469 from ionite34/merge-main-to-dev-03e9e54 10 months ago
Ionite 116f5dc905
More duplicates 10 months ago
Ionite 3f10a705d4
Poof duplicate function 10 months ago
Ionite e5b316ca48
Poof duplicate function 10 months ago
Ionite 843b83d0e9
Merge branch 'dev' into merge-main-to-dev-03e9e54 10 months ago
Ionite 03e9e5417c
Merge pull request #468 from ionite34/backport/main/pr-465_pr-465_pr-465_pr-465_pr-465_pr-465_pr-465_pr-465_pr-465_pr-465_pr-465 10 months ago
Ionite c548ea43e3
Add CollectionBuilder for ProcessArgs 10 months ago
Ionite f41f5b1fde
Update CHANGELOG.md 10 months ago
JT dd1e1cec3c
install invoke frontend on launch if its not there (for people who updated before the fix) 10 months ago
jt a40d820d6a
finally fix invoke build on mac/linux 10 months ago
JT 5f443ace4b
moar path 10 months ago
JT 57a087f95e
fix windows 10 months ago
JT afa92b7b57
path delimiter 10 months ago
JT f663b101ba
moar fixes & add to path 10 months ago
JT 7d2675d18e
fix npm path & update windows folder name 10 months ago
ionite34 30d840afb4
Fix visibility 10 months ago
JT bc1b912c7a
extractManaged 10 months ago
JT 713e32473a
add npm stuff for linux/macos 10 months ago
JT 7dbc5d53ae
install node/pnpm and build invoke frontend during install 10 months ago
JT 1a4aa0dc15
Merge pull request #465 from ionite34/fix-invoke 10 months ago
JT 19938536e4 fix package install button being enabled 24/7 10 months ago
JT 027f4175f4 chagenlog 10 months ago
JT 6eca39bc5e install invoke frontend on launch if its not there (for people who updated before the fix) 10 months ago
JT da304547f4
Merge branch 'dev' into fix-invoke 10 months ago
jt 4e21fd9c3a finally fix invoke build on mac/linux 10 months ago
JT c242bf1435 moar path 10 months ago
Ionite eda036ce69
Merge pull request #466 from ionite34/fix-startapp 10 months ago
ionite34 226dcc9cf2
Fix non-macos StartApp filename and args 10 months ago
JT 3ab6a2cf41 fix windows 10 months ago
JT b70cd8a549 path delimiter 10 months ago
JT c10c04a5c5 moar fixes & add to path 10 months ago
JT 95c2e43470 fix npm path & update windows folder name 10 months ago
ionite34 5f874183d1
Fix visibility 10 months ago
JT 7779739ed6 extractManaged 10 months ago
JT 0325fe01a6 add npm stuff for linux/macos 10 months ago
JT 4e0026f7d2
Merge branch 'dev' into fix-invoke 10 months ago
JT 2f55c80996 install node/pnpm and build invoke frontend during install 10 months ago
JT b587bac08d
Merge pull request #464 from ionite34/fix-logging 10 months ago
JT 6f144714a8
Merge branch 'dev' into fix-logging 10 months ago
JT fbe3eb176b do some .ToSTrings() on DirectoryPaths so they dont enumerate & log every item 10 months ago
JT cb94fabf84
Merge pull request #463 from ionite34/downmerge 10 months ago
JT aa97ac9fcb
Merge branch 'dev' into downmerge 10 months ago
Ionite 8945b409d6
Merge pull request #456 from ionite34/package-extensions 10 months ago
JT cfe2be8453
Merge branch 'dev' into downmerge 10 months ago
JT ca6974894e
Merge pull request #397 from LykosAI/main 10 months ago
Ionite 0b88add34d
Merge pull request #462 from ionite34/update-changelog 10 months ago
Ionite 0210d59fd4
Update CHANGELOG.md 10 months ago
Ionite 6a34795c8e
Merge pull request #461 from ionite34/backport/main/pr-434 10 months ago
ionite34 34908a2f00
Allow multiple arguments in pip install args 10 months ago
Ionite 0feb679d94
Merge pull request #460 from ionite34/downmerge-fork 10 months ago
ionite34 7e8cb3298a
Update CHANGELOG.md 10 months ago
ionite34 1eff8c0969
Add before install extension notice 10 months ago
ionite34 8be10bd9d1
Add requirements install for comfy extensions 10 months ago
ionite34 9ed0bf67ce
Filter and indicator for already installed extensions 10 months ago
JT 6dc34207f6 add automatic1111 extensions too 10 months ago
JT 8f1d28bd3f make extension checkbox toggle when you click anywhere & make downloads tab taller 10 months ago
ionite34 3da476fa2e
Update CHANGELOG.md 10 months ago
ionite34 76660fdb03
Extensions menu separator and visibility 10 months ago
Ionite e04f3ef75c
Merge branch 'dev' into package-extensions 10 months ago
ionite34 9da30935f1
Use verbose delete for extension uninstall 10 months ago
ionite34 cec7342f6d
Move DirectoryPathExtensions to Core, add cancel token 10 months ago
JT e071006475
Merge pull request #458 from ionite34/checkpoint-updates-n-stuff 10 months ago
JT 7c0a100b54
Merge pull request #395 from rookiepsi/fix-invokeai-torch 10 months ago
psi a9519cdbc6 Fix pytorch version used in invokeai 10 months ago
Ionite 7bcd1ec5fa
Merge branch 'dev' into package-extensions 10 months ago
ionite34 7013f81311
Add extension settings 10 months ago
JT 20ff29dee6 localize & chagenlog 10 months ago
JT c4b4fe1e30 dont .tolist 10 months ago
JT 03ab4346f1 Merge branch 'checkpoint-updates-n-stuff' of https://github.com/ionite34/StabilityMatrix into checkpoint-updates-n-stuff 10 months ago
JT 9402983b92 refactor & un-async Index method 10 months ago
JT ae81fd0ed9
Merge branch 'dev' into checkpoint-updates-n-stuff 10 months ago
JT 3881c2e52d add Find in Model Browser button on Checkpoints page context menu & some backend stuff for checking model updates 10 months ago
ionite34 00e8705799
Add installed extension updating 10 months ago
ionite34 98cd09eb85
Update TextBox using search class 10 months ago
ionite34 bcb47ae8f0
Add missing FuncCommandConverter 10 months ago
ionite34 4c8d7143b3
Add TextBoxStyles and `search` class 10 months ago
ionite34 a3155de7f8
Add installed extension uninstall 10 months ago
ionite34 e4198a1516
Fix DirectoryPath delete via IPathObject 10 months ago
ionite34 9b3130e662
Fix search symbol visibility 10 months ago
ionite34 38565917bc
Update .gitignore 10 months ago
ionite34 e27a00b814
Add Installed package extension managing 10 months ago
ionite34 0ebece2b91
HyperlinkIconButton custom icon and file opening 10 months ago
ionite34 62b1c796fb
Add FileUriConverter 10 months ago
ionite34 26e5a4009d
Add SearchCollection for DynamicData searching 10 months ago
JT b85e444dde
Merge pull request #457 from ionite34/base-model-filter 10 months ago
JT 5d7f8dff53 disable existing huggingface download checkmark button things 10 months ago
JT 5a39c274ba fix "other" category 10 months ago
JT 0f190f2cf6 make filter work 10 months ago
JT bd8d028c89 fix list clear getting saved to settings & add chagenlog 10 months ago
JT 9b51e25808 use observable stuffs for CheckpointFolder base model selection & save it to settings 10 months ago
JT f0fba442ee added base model filter to checkpoints page (wip) 11 months ago
ionite34 d991f4cd62
Refresh on load and add clear selection method 11 months ago
ionite34 7cc88e806b
Fix BetterContentDialog CloseOnClickOutside for other vm types 11 months ago
ionite34 21133b758c
Add PackageExtensionBrowser 11 months ago
ionite34 a4c895080a
Add generic SelectableItem vm 11 months ago
ionite34 b4779cd725
Update font setting 11 months ago
ionite34 df55166804
Fallback fonts on macos 11 months ago
ionite34 137eca4950
Add extension management backends 11 months ago
ionite34 332f39725f
Add DownloadService.GetContentAsync 11 months ago
ionite34 5cf8b053f6
Add CheckIsGitRepository, fix vlad git impl 11 months ago
Ionite ea662e7937
Merge pull request #455 from ionite34/fix-macos-update 11 months ago
ionite34 8e728be4e3
Fix comment 11 months ago
ionite34 9fe0ef8689
macos update compat fixes 11 months ago
ionite34 36848899f0
Add Move and Copy methods for DirectoryPath 11 months ago
JT f03f7d3b60
Merge pull request #383 from LykosAI/main 11 months ago
JT 1daa7d19a9 Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix 11 months ago
JT 2072701c73 Merge branch 'main' of https://github.com/LykosAI/StabilityMatrix-Dev 11 months ago
JT 4db3520a71
Merge pull request #454 from ionite34/downmerge 11 months ago
JT e5aedc1b18
Merge branch 'dev' into downmerge 11 months ago
JT 1332b8d63c
Merge pull request #453 from ionite34/oops-main 11 months ago
JT cebdc28840 fix directml showing when it shouldnt 11 months ago
JT 7c06631fa7
Merge pull request #451 from ionite34/downmerge 11 months ago
JT fc6ef780e7 fix build errors 11 months ago
JT 447854c107
Merge branch 'dev' into downmerge 11 months ago
Ionite e0a2db5289
Merge pull request #447 from ionite34/package-manager 11 months ago
Ionite 679cf47ac1
Merge branch 'dev' into package-manager 11 months ago
JT 6438048c1a
Merge pull request #448 from ionite34/fixes 11 months ago
JT 04e38d2fdc chagenlog 11 months ago
JT 8e526922d2
Merge branch 'dev' into fixes 11 months ago
JT 871a0cda5c - Added `--use-directml` launch argument for SDWebUI DirectML fork 11 months ago
ionite34 ae9a9d3f61
Fix ConnectionHelp navigation to installer 11 months ago
ionite34 bd128a0936
cleanup 11 months ago
ionite34 bc463dfbbf
Fix exit freeze on macos 11 months ago
ionite34 b6a5d0516c
Organize namespaces 11 months ago
JT a5b17de63b
Merge pull request #446 from ionite34/new-installer-flow 11 months ago
JT 3a2223cdda extensions ded 11 months ago
JT f582bf4d76 ionite killed comfy manager & I added search to new packages page 11 months ago
JT a11c2b20fd also set RecommendedSharedFolderMethod 11 months ago
JT 82004934aa default to recommended torch version & show disclaimers 11 months ago
JT 3c425d4a7f added ShowIncompatiblePackages toggle 11 months ago
JT 177d9cb024 New installer flow with tabs n extensions n stuff 11 months ago
Ionite e1c25c1a5c
Merge pull request #445 from ionite34/macos-update 11 months ago
Ionite 5482a043bf
Merge branch 'dev' into macos-update 11 months ago
JT 45024abea2
Merge pull request #444 from ionite34/back-button 11 months ago
JT 96571a7c9b Fixed back button navigation 11 months ago
ionite34 50d25b8eb8
Update CHANGELOG.md 11 months ago
ionite34 f5819aa5b7
Add macos update with dmg extract 11 months ago
ionite34 8129817da4
Add dmg extract methods 11 months ago
ionite34 9f5aa9b815
Support CollectionBuilder for ProcessArgs 11 months ago
ionite34 0bdd46d110
Add EnsureSuccessExitCode for ProcessResult tasks 11 months ago
ionite34 4991960222
Add RelativeTo file path methods 11 months ago
Ionite 23c1dc1096
Merge pull request #441 from ionite34/ci-fixes 11 months ago
ionite34 31e234abf1
Add fix changelog 11 months ago
ionite34 fb4c3a2ac3
Downgrade avalonia to 11.0.5 to fix datagrid edit 11 months ago
ionite34 b05aaa56ed
Merge branch 'ci-fixes' of https://github.com/ionite34/StabilityMatrix into ci-fixes 11 months ago
ionite34 031f0f77f7
Add macos builds changelog 11 months ago
Ionite 648273d6f6
Merge branch 'dev' into ci-fixes 11 months ago
JT 582eae0d84
Merge pull request #443 from ionite34/fix-import-free-space 11 months ago
JT a84c9110f5
Merge branch 'dev' into fix-import-free-space 11 months ago
JT b619270b55 allow import if free space is null 11 months ago
JT ed543360df
Merge pull request #442 from ionite34/fix-onetrainer-compatibility 11 months ago
JT a763e2cd1d Dont show onetrainer unless nvidia 11 months ago
ionite34 1d486ff294
Add macos to UpdatePlatforms 11 months ago
ionite34 5a6c3ca796
Fix shutdown freezing on macos 11 months ago
ionite34 10b569a5ca
apparently exceeds workflow input limits 11 months ago
ionite34 0d03591f24
Add platform skipping 11 months ago
ionite34 4f931624cd
Add embedded signing script 11 months ago
ionite34 99d3ac5e78
Update python hash 11 months ago
ionite34 ff47ffa105
Update README for macos 11 months ago
ionite34 fd4d2fd83d
Update to zip python distribution 11 months ago
JT 8b359e045f
Merge pull request #439 from ionite34/dumb-tests 11 months ago
JT 3c96a607a7
Merge branch 'dev' into dumb-tests 11 months ago
ionite34 a1655dc28d
Add unzip to other steps 11 months ago
ionite34 e47b7da484
Fix some more file names 11 months ago
ionite34 34ef13b513
Use cpython with signed dylib on macos 11 months ago
ionite34 8cc50aff0a
Fixed dmg rename and working dirs 11 months ago
ionite34 7e3aaed219
Quoting 11 months ago
ionite34 fd68b85420
Add test release artifacts action 11 months ago
ionite34 8799f8464d
Fix artifact zipping 11 months ago
ionite34 07af9d416a
Fix some file naming 11 months ago
ionite34 db2876509e
Fix more quoting 11 months ago
ionite34 b2385fb6c5
Fix some path quoting 11 months ago
ionite34 bf8692df8b
Use relative entitlements file path to file 11 months ago
ionite34 b9efb34153
Fix env var setting on macos ci 11 months ago
ionite34 a98f6eb673
Add DotNetBundle to gif 11 months ago
ionite34 c812aa623c
add NuGetVersion 11 months ago
ionite34 2b839aca86
Update release.yml 11 months ago
ionite34 11a0de8cbb
Change env var quotes 11 months ago
ionite34 a6f9ea48b8
change macos build version 11 months ago
ionite34 e3405887b1
Fix macos build maybe 11 months ago
Ionite 6105b49eaf
Merge pull request #440 from ionite34/fix-macos-ci 11 months ago
ionite34 b24d78a428
Hide memory available where not supported 11 months ago
ionite34 07d7a43aa5
Fix shutdown freeze on macos 11 months ago
ionite34 7fe58b6f60
Add TotalInstalledBytes using physical 11 months ago
ionite34 c023666061
Update compat check for macos 11 months ago
ionite34 60b143c89f
Stop timer if live memory info not supported 11 months ago
ionite34 9986d99f1a
Support memory info and gpu info on macos 11 months ago
ionite34 bf8bb11212
Use TotalPhysical for ram on macos 11 months ago
ionite34 71fa99ce46
Fix env var access 11 months ago
ionite34 8902f174ac
Remove unused entitlement permission 11 months ago
Ionite 6fd3244b75
Merge pull request #438 from ionite34/macos-build 11 months ago
ionite34 8f72aa9ebe
Add macos releases to upload 11 months ago
JT 552af92943 idk 11 months ago
ionite34 2ec6efc431
Add CFBundlePackageType 11 months ago
JT 7006e43f2b missed some files 11 months ago
JT 96317b3ecd more ui tests 11 months ago
ionite34 6dcdf1aba9
Fix some stuff, add AppEntitlements 11 months ago
ionite34 2c32a1c43f
Copy AppIcon to output on macos 11 months ago
ionite34 d70e5c922a
Add macos app icon 11 months ago
ionite34 c0d5e2acd8
Add macos sign notarize release CIs 11 months ago
JT 9b878d4ef3
Merge pull request #437 from ionite34/add-onetrainer 11 months ago
JT 1f6c947102 add onetrainer & moved tk/tcl env vars to BaseGitPackage 11 months ago
JT 6ae9acf474
Merge pull request #436 from ionite34/delete-from-dialog 11 months ago
JT 24ad22463f Added Delete button to model browser SelectModelVersionDialog & added 'copy link to clipboard' for connected models 11 months ago
Ionite 0972e7dd4a
Merge pull request #435 from ionite34/model-indexing 11 months ago
ionite34 22aa9cd68d
Merge branch 'model-indexing' of https://github.com/ionite34/StabilityMatrix into model-indexing 11 months ago
ionite34 b9efeb46b2
Add find local model from index debug command 11 months ago
ionite34 a691773375
Add AutoCtor 11 months ago
Ionite d61b6ab8d5
Merge branch 'dev' into model-indexing 11 months ago
Ionite 9d36794213
Merge pull request #434 from ionite34/pip-fix-macos 11 months ago
Ionite debe3cb3f4
Merge branch 'dev' into pip-fix-macos 11 months ago
Ionite 790b2043d6
Add model index functions 11 months ago
ionite34 d12cab97b1
Update CHANGELOG.md 11 months ago
ionite34 384d1f3746
No set cpu on macos 11 months ago
ionite34 a125a0bd59
Add VAE precision to comfy args 11 months ago
ionite34 5e0a948009
Add force fp config for comfy 11 months ago
ionite34 7d247166ee
Add Mps recommend in BasePackage 11 months ago
ionite34 661e387a88
Add cross attention method args for comfy 11 months ago
ionite34 b60ccf9f36
Formatting 11 months ago
ionite34 f94bc50a2c
Skip URI registration on macos 11 months ago
ionite34 b0a4f2e82e
Update CHANGELOG.md 11 months ago
ionite34 868edbffd1
Allow multiple arguments in pip install args 11 months ago
ionite34 7a939db678
Fix pip install error on macos 11 months ago
JT 0a9b590eba
Merge pull request #384 from ionite34/image-to-video 11 months ago
JT 9275a7ad32 remove unused imageMagick lib 11 months ago
JT 8cd3ec1b98 chagenlog & preview fix 11 months ago
JT eecea73aea Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into image-to-video 11 months ago
JT 02bd059ad6
Merge pull request #431 from ionite34/downmerge 11 months ago
JT b0e4a09953
Merge branch 'dev' into downmerge 11 months ago
JT 7ef86731a6 moved strings to resources & added send to img2img/vid options in output browser 11 months ago
Ionite d9ff373ca5
Merge pull request #429 from ionite34/chagenlog-update 11 months ago
Ionite b4e4886249
Update CHANGELOG.md 11 months ago
JT b13bea5b6f add webps to outputs page 11 months ago
JT a8894f9631 save/load img2vid and show webps in dialog thing 11 months ago
ionite34 25a1a40f61
Use ImageGalleryCard for ImageToVideo 11 months ago
ionite34 96f722a9da
Add WebpAnimation support in ImageBoxView 11 months ago
JT 9e28cb6965 save metadata 11 months ago
JT 1bd293d8cd Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into image-to-video 11 months ago
Ionite 65b3785818
Merge pull request #428 from ionite34/merge-main-to-dev-70e9b9a 11 months ago
Ionite 70c0f0d523
Merge branch 'dev' into merge-main-to-dev-70e9b9a 11 months ago
JT 1b06732c92 Fix merge/build errors 11 months ago
JT ad557837d8 Merge branch 'image-to-video' of https://github.com/ionite34/StabilityMatrix into image-to-video 11 months ago
JT d533be3329 handle window size/position outside of shutdown, load pngs faster, start of saving some img2vid stuff, webp metadata reading, and different settings write method (that still doesn't entirely work and occasionally adds extra right curly brackets to the end of the file) 11 months ago
JT d271d95ea0
Merge branch 'dev' into image-to-video 11 months ago
Ionite 35f30f9518
Merge pull request #413 from ionite34/inference-clip-skip 11 months ago
Ionite ee66f0dc49
Made default markdown dialog size larger 11 months ago
Ionite f6a1a241eb
Improved Model card spacing a bit 11 months ago
Ionite 47c663113b
Merge branch 'dev' into inference-clip-skip 11 months ago
JT 816529309a Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into image-to-video 11 months ago
JT 724c3f47cf
Merge pull request #425 from ionite34/downmerge 11 months ago
JT ed9c5b6cfb
Merge branch 'dev' into downmerge 11 months ago
Ionite 0f52bcb3a8
Fix combobox styles 11 months ago
Ionite 280fc8c89f
Add animated webp rendering 11 months ago
JT a2d8040310 faster gif conversion 11 months ago
JT ff4c347567 added webp to gif conversion & display in img2vid 11 months ago
JT 2f6deecf51 Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into image-to-video 11 months ago
Ionite 2e36f48f6b
Add localized strings 11 months ago
Ionite 1afe3e6347
Add improved model card combo box styles 11 months ago
Ionite a6c87a2be3
Add BetterComboBox 11 months ago
Ionite ed4f78a57e
Add clip skip default value 11 months ago
Ionite 1c37e9b4e4
Version bump 11 months ago
Ionite d82f3d4843
Fix Value access 11 months ago
Ionite 5eaaedf4dd
Add clip skip selection to ModelCard control 11 months ago
Ionite 5871241d6f
Add Clip skip and model connection refactors 11 months ago
Ionite 74aac6272b
Add NullableExtensions.Unwrap struct overload 11 months ago
Ionite 097845a709
Add NullableExtensions.Unwrap 11 months ago
Ionite c5bda38264
Add CLIPSetLastLayer Node 11 months ago
JT 5267d8bcef
Merge pull request #412 from ionite34/downmerge 11 months ago
JT f7feb2e933
Merge branch 'dev' into downmerge 11 months ago
JT 7876df385f
Merge pull request #346 from ionite34/mac-fixes 11 months ago
JT e92a506c79
Merge branch 'dev' into mac-fixes 11 months ago
JT 6154d835ee add header to VideoOutputSettingsCard to indicate it's for output settings 12 months ago
JT e389085ec1 Added image to video in inference (mostly) 12 months ago
jt b4fd64e7fc macos fixes & remove mac label for volta 1 year ago
  1. 37
      .github/workflows/backport.yml
  2. 205
      .github/workflows/release.yml
  3. 1
      .gitignore
  4. 18
      Avalonia.Gif/Avalonia.Gif.csproj
  5. 10
      Avalonia.Gif/BgWorkerCommand.cs
  6. 12
      Avalonia.Gif/BgWorkerState.cs
  7. 10
      Avalonia.Gif/Decoding/BlockTypes.cs
  8. 8
      Avalonia.Gif/Decoding/ExtensionType.cs
  9. 10
      Avalonia.Gif/Decoding/FrameDisposal.cs
  10. 36
      Avalonia.Gif/Decoding/GifColor.cs
  11. 653
      Avalonia.Gif/Decoding/GifDecoder.cs
  12. 20
      Avalonia.Gif/Decoding/GifFrame.cs
  13. 19
      Avalonia.Gif/Decoding/GifHeader.cs
  14. 43
      Avalonia.Gif/Decoding/GifRect.cs
  15. 8
      Avalonia.Gif/Decoding/GifRepeatBehavior.cs
  16. 23
      Avalonia.Gif/Decoding/InvalidGifStreamException.cs
  17. 23
      Avalonia.Gif/Decoding/LzwDecompressionException.cs
  18. 81
      Avalonia.Gif/Extensions/StreamExtensions.cs
  19. 297
      Avalonia.Gif/GifImage.cs
  20. 147
      Avalonia.Gif/GifInstance.cs
  21. 15
      Avalonia.Gif/IGifInstance.cs
  22. 20
      Avalonia.Gif/InvalidGifStreamException.cs
  23. 180
      Avalonia.Gif/WebpInstance.cs
  24. 8
      Build/AppEntitlements.entitlements
  25. 12
      Build/EmbeddedEntitlements.entitlements
  26. 26
      Build/build_macos_app.sh
  27. 62
      Build/codesign_embedded_macos.sh
  28. 37
      Build/codesign_macos.sh
  29. 32
      Build/notarize_macos.sh
  30. 445
      CHANGELOG.md
  31. 5
      Directory.Build.props
  32. 76
      README.md
  33. 13
      StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
  34. 12
      StabilityMatrix.Avalonia/App.axaml
  35. 240
      StabilityMatrix.Avalonia/App.axaml.cs
  36. 28
      StabilityMatrix.Avalonia/Assets.cs
  37. BIN
      StabilityMatrix.Avalonia/Assets/AppIcon.icns
  38. 11
      StabilityMatrix.Avalonia/Assets/hf-packages.json
  39. 14
      StabilityMatrix.Avalonia/Assets/sitecustomize.py
  40. 141
      StabilityMatrix.Avalonia/Collections/SearchCollection.cs
  41. 35
      StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml
  42. 32
      StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml.cs
  43. 38
      StabilityMatrix.Avalonia/Controls/BetterComboBox.cs
  44. 62
      StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs
  45. 208
      StabilityMatrix.Avalonia/Controls/BetterContextDragBehavior.cs
  46. 46
      StabilityMatrix.Avalonia/Controls/DataTemplateSelector.cs
  47. 113
      StabilityMatrix.Avalonia/Controls/HyperlinkIconButton.cs
  48. 78
      StabilityMatrix.Avalonia/Controls/Inference/ControlNetCard.axaml
  49. 22
      StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml
  50. 9
      StabilityMatrix.Avalonia/Controls/Inference/ImageGalleryCard.axaml
  51. 48
      StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml
  52. 7
      StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml.cs
  53. 190
      StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml
  54. 60
      StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml
  55. 2
      StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml.cs
  56. 96
      StabilityMatrix.Avalonia/Controls/Inference/PromptExpansionCard.axaml
  57. 52
      StabilityMatrix.Avalonia/Controls/Inference/PromptExpansionCard.axaml.cs
  58. 8
      StabilityMatrix.Avalonia/Controls/Inference/SamplerCard.axaml
  59. 2
      StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml
  60. 6
      StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml
  61. 6
      StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml
  62. 2
      StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml
  63. 6
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml
  64. 8
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs
  65. 4
      StabilityMatrix.Avalonia/Controls/StarsRating.axaml
  66. 15
      StabilityMatrix.Avalonia/Controls/StarsRating.axaml.cs
  67. 65
      StabilityMatrix.Avalonia/Controls/TreeFileExplorer.axaml
  68. 135
      StabilityMatrix.Avalonia/Controls/TreeFileExplorer.axaml.cs
  69. 20
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/AsyncImageFailedEventArgs.cs
  70. 42
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Events.cs
  71. 135
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Properties.cs
  72. 248
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.cs
  73. 565
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheBase.cs
  74. 18
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheOptions.cs
  75. 36
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/FileCache.cs
  76. 76
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/ImageCache.cs
  77. 156
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorage.cs
  78. 49
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorageItem.cs
  79. 21
      StabilityMatrix.Avalonia/Controls/VendorLabs/LICENSE
  80. 48
      StabilityMatrix.Avalonia/Controls/VendorLabs/Themes/BetterAsyncImage.axaml
  81. 122
      StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml
  82. 7
      StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml.cs
  83. 98
      StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml
  84. 7
      StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml.cs
  85. 40
      StabilityMatrix.Avalonia/Converters/EnumAttributeConverter.cs
  86. 13
      StabilityMatrix.Avalonia/Converters/EnumAttributeConverters.cs
  87. 37
      StabilityMatrix.Avalonia/Converters/FileUriConverter.cs
  88. 53
      StabilityMatrix.Avalonia/Converters/FuncCommandConverter.cs
  89. 344
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  90. 21
      StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs
  91. 22
      StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs
  92. 19
      StabilityMatrix.Avalonia/DesignData/MockSettingsManager.cs
  93. 49
      StabilityMatrix.Avalonia/DialogHelper.cs
  94. 38
      StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs
  95. 10
      StabilityMatrix.Avalonia/Extensions/DataObjectExtensions.cs
  96. 20
      StabilityMatrix.Avalonia/Extensions/NotificationLevelExtensions.cs
  97. 54
      StabilityMatrix.Avalonia/Extensions/NotificationServiceExtensions.cs
  98. 9
      StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs
  99. 24
      StabilityMatrix.Avalonia/Helpers/IOCommands.cs
  100. 294
      StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs
  101. Some files were not shown because too many files have changed in this diff Show More

37
.github/workflows/backport.yml

@ -0,0 +1,37 @@
name: Automatic Backport
on:
pull_request:
types: ["closed", "labeled"]
jobs:
backport:
if: ${{ (github.event.pull_request.merged == true) && (contains(github.event.pull_request.labels.*.name, 'backport-to-main') == true) }}
name: Backport PR
runs-on: ubuntu-latest
steps:
- name: Write json
id: create-json
uses: jsdaniell/create-json@v1.2.3
with:
name: ".backportrc.json"
json: |
{
"targetPRLabels": "backport",
"prTitle": "[{{sourceBranch}} to {{targetBranch}}] backport: {{sourcePullRequest.title}} ({{sourcePullRequest.number}})"
}
- name: Backport Action
uses: sorenlouv/backport-github-action@v9.3.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
auto_backport_label_prefix: backport-to-
- name: Info log
if: ${{ success() }}
run: cat ~/.backport/backport.info.log
- name: Debug log
if: ${{ failure() }}
run: cat ~/.backport/backport.debug.log

205
.github/workflows/release.yml

@ -43,6 +43,10 @@ on:
- stable
- preview
- development
test-release-artifacts:
type: boolean
description: "[Debug] Test release artifacts?"
default: false
jobs:
release-linux:
@ -72,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:
@ -172,10 +171,119 @@ jobs:
name: StabilityMatrix-${{ env.platform-id }}
path: ./out/${{ env.out-name }}
release-macos:
name: Release (macos-arm64)
env:
platform-id: osx-arm64
app-name: "Stability Matrix.app"
out-name: "StabilityMatrix-macos-arm64.dmg"
runs-on: macos-14
steps:
- uses: actions/checkout@v3
- uses: olegtarasov/get-tag@v2.1.2
if: github.event_name == 'release'
id: tag_name
with:
tagRegex: "v(.*)"
- name: Set Version from Tag
if: github.event_name == 'release'
run: |
echo "Using tag ${{ env.GIT_TAG_NAME }}"
echo "RELEASE_VERSION=${{ env.GIT_TAG_NAME }}" >> $GITHUB_ENV
- name: Set Version from manual input
if: github.event_name == 'workflow_dispatch'
run: |
echo "Using version ${{ github.event.inputs.version }}"
echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
- name: Set up .NET 8
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Install dependencies
run: dotnet restore -p:PublishReadyToRun=true
- name: Check Version
run: echo $RELEASE_VERSION
- name: .NET Msbuild (App)
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: >
dotnet msbuild ./StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
-t:BundleApp -p:UseAppHost=true -p:SelfContained=true
-p:Configuration=Release -p:RuntimeIdentifier=${{ env.platform-id }}
-p:Version=$RELEASE_VERSION
-p:PublishDir=out
-p:PublishReadyToRun=true
-p:CFBundleShortVersionString=$RELEASE_VERSION
-p:CFBundleName="Stability Matrix"
-p:CFBundleDisplayName="Stability Matrix"
-p:CFBundleVersion=$RELEASE_VERSION
-p:SentryOrg=${{ secrets.SENTRY_ORG }} -p:SentryProject=${{ secrets.SENTRY_PROJECT }}
-p:SentryUploadSymbols=true -p:SentryUploadSources=true
- name: Post Build (App)
run: mkdir -p signing && mv "./StabilityMatrix.Avalonia/out/Stability Matrix.app" "./signing/${{ env.app-name }}"
- name: Codesign app bundle
env:
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
run: ./Build/codesign_macos.sh "./signing/${{ env.app-name }}"
- name: Notarize app bundle
env:
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
run: ./Build/notarize_macos.sh "./signing/${{ env.app-name }}"
- name: Zip Artifact (App)
working-directory: signing
run: zip -r -y "../StabilityMatrix-${{ env.platform-id }}-app.zip" "${{ env.app-name }}"
- name: Upload Artifact (App)
uses: actions/upload-artifact@v2
with:
name: StabilityMatrix-${{ env.platform-id }}-app
path: StabilityMatrix-${{ env.platform-id }}-app.zip
- uses: actions/setup-node@v4
with:
node-version: '20.11.x'
- name: Install dependencies for dmg creation
run: brew install graphicsmagick imagemagick && npm install --global create-dmg
- name: Create dmg
working-directory: signing
run: >
create-dmg "${{ env.app-name }}" --overwrite --identity "${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}"
- name: Rename dmg
working-directory: signing
run: mv "$(find . -type f -name "*.dmg")" "${{ env.out-name }}"
- name: Zip Artifact (dmg)
working-directory: signing
run: zip -r -y "../StabilityMatrix-${{ env.platform-id }}-dmg.zip" "${{ env.out-name }}"
- name: Upload Artifact (dmg)
uses: actions/upload-artifact@v2
with:
name: StabilityMatrix-${{ env.platform-id }}-dmg
path: StabilityMatrix-${{ env.platform-id }}-dmg.zip
publish-release:
name: Publish GitHub Release
needs: [ release-linux, release-windows ]
needs: [ release-linux, release-windows, release-macos ]
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.github-release == 'true' }}
runs-on: ubuntu-latest
steps:
@ -194,11 +302,12 @@ jobs:
- name: Download Artifacts
uses: actions/download-artifact@v3
# Zip each build
# Zip each build (except macos which is already dmg)
- name: Zip Artifacts
run: |
cd StabilityMatrix-win-x64 && zip -r ../StabilityMatrix-win-x64.zip ./. && cd $OLDPWD
cd StabilityMatrix-linux-x64 && zip -r ../StabilityMatrix-linux-x64.zip ./. && cd $OLDPWD
unzip "StabilityMatrix-osx-arm64-dmg/StabilityMatrix-osx-arm64-dmg.zip"
- name: Create Github Release
id: create_release
@ -209,15 +318,75 @@ jobs:
files: |
StabilityMatrix-win-x64.zip
StabilityMatrix-linux-x64.zip
StabilityMatrix-macos-arm64.dmg
fail_on_unmatched_files: true
tag_name: v${{ github.event.inputs.version }}
body: ${{ steps.release_notes.outputs.release_notes }}
draft: ${{ github.event.inputs.github-release-draft == 'true' }}
prerelease: ${{ github.event.inputs.github-release-prerelease == 'true' }}
test-artifacts:
name: Test Release Artifacts
needs: [ release-linux, release-windows, release-macos ]
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.test-release-artifacts == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Extract Release Notes
id: release_notes
run: |
RELEASE_NOTES="$(awk -v version="${{ github.event.inputs.version }}" '/## v/{if(p) exit; if($0 ~ version) p=1}; p' CHANGELOG.md)"
RELEASE_NOTES="${RELEASE_NOTES//'%'/'%25'}"
RELEASE_NOTES="${RELEASE_NOTES//$'\n'/'%0A'}"
RELEASE_NOTES="${RELEASE_NOTES//$'\r'/'%0D'}"
echo "::set-output name=release_notes::$RELEASE_NOTES"
echo "Release Notes:"
echo "$RELEASE_NOTES"
# Downloads all previous artifacts to the current working directory
- name: Download Artifacts
uses: actions/download-artifact@v3
# Zip each build (except macos which is already dmg)
- name: Zip Artifacts
run: |
cd StabilityMatrix-win-x64 && zip -r ../StabilityMatrix-win-x64.zip ./. && cd $OLDPWD
cd StabilityMatrix-linux-x64 && zip -r ../StabilityMatrix-linux-x64.zip ./. && cd $OLDPWD
unzip "StabilityMatrix-osx-arm64-dmg/StabilityMatrix-osx-arm64-dmg.zip"
# Check that the zips and CHANGELOG.md are in the current working directory
- name: Check files
run: |
if [ ! -f StabilityMatrix-win-x64.zip ]; then
echo "StabilityMatrix-win-x64.zip not found"
exit 1
else
echo "StabilityMatrix-win-x64.zip found"
sha256sum StabilityMatrix-win-x64.zip
fi
if [ ! -f StabilityMatrix-linux-x64.zip ]; then
echo "StabilityMatrix-linux-x64.zip not found"
exit 1
else
echo "StabilityMatrix-linux-x64.zip found"
sha256sum StabilityMatrix-linux-x64.zip
fi
if [ ! -f StabilityMatrix-macos-arm64.dmg ]; then
echo "StabilityMatrix-macos-arm64.dmg not found"
exit 1
else
echo "StabilityMatrix-macos-arm64.dmg found"
sha256sum StabilityMatrix-macos-arm64.dmg
fi
if [ ! -f CHANGELOG.md ]; then
echo "CHANGELOG.md not found"
exit 1
fi
publish-auto-update-github:
name: Publish Auto-Update Release (GitHub)
needs: [ release-linux, release-windows ]
needs: [ release-linux, release-windows, release-macos ]
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.auto-update-release == 'true' && github.event.inputs.auto-update-release-mode == 'github url' }}
runs-on: ubuntu-latest
steps:
@ -233,7 +402,7 @@ jobs:
python-version: '3.11'
- name: Install Python Dependencies
run: pip install stability-matrix-tools>=0.2.18 --upgrade
run: pip install stability-matrix-tools>=0.3.0 --upgrade
- name: Publish Auto-Update Release
env:
@ -246,7 +415,7 @@ jobs:
publish-auto-update-b2:
name: Publish Auto-Update Release (B2)
needs: [ release-linux, release-windows ]
needs: [ release-linux, release-windows, release-macos ]
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.auto-update-release == 'true' && github.event.inputs.auto-update-release-mode == 'upload to b2' }}
runs-on: ubuntu-latest
steps:
@ -261,18 +430,19 @@ jobs:
- name: Download Artifacts
uses: actions/download-artifact@v3
# Zip each build
# Zip each build (except macos which is already dmg)
- name: Zip Artifacts
run: |
cd StabilityMatrix-win-x64 && zip -r ../StabilityMatrix-win-x64.zip ./. && cd $OLDPWD
cd StabilityMatrix-linux-x64 && zip -r ../StabilityMatrix-linux-x64.zip ./. && cd $OLDPWD
unzip "StabilityMatrix-osx-arm64-dmg/StabilityMatrix-osx-arm64-dmg.zip"
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install Python Dependencies
run: pip install stability-matrix-tools>=0.2.18 --upgrade
run: pip install stability-matrix-tools>=0.3.0 --upgrade
# Check that the zips and CHANGELOG.md are in the current working directory
- name: Check files
@ -285,6 +455,10 @@ jobs:
echo "StabilityMatrix-linux-x64.zip not found"
exit 1
fi
if [ ! -f StabilityMatrix-macos-arm64.dmg ]; then
echo "StabilityMatrix-macos-arm64.dmg not found"
exit 1
fi
if [ ! -f CHANGELOG.md ]; then
echo "CHANGELOG.md not found"
exit 1
@ -303,4 +477,5 @@ jobs:
--changelog CHANGELOG.md
--win-x64 StabilityMatrix-win-x64.zip
--linux-x64 StabilityMatrix-linux-x64.zip
--macos-arm64 StabilityMatrix-macos-arm64.dmg
-y

1
.gitignore vendored

@ -397,3 +397,4 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
.husky/pre-commit

18
Avalonia.Gif/Avalonia.Gif.csproj

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="SkiaSharp" Version="2.88.7" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
</ItemGroup>
</Project>

10
Avalonia.Gif/BgWorkerCommand.cs

@ -0,0 +1,10 @@
namespace Avalonia.Gif
{
internal enum BgWorkerCommand
{
Null,
Play,
Pause,
Dispose
}
}

12
Avalonia.Gif/BgWorkerState.cs

@ -0,0 +1,12 @@
namespace Avalonia.Gif
{
internal enum BgWorkerState
{
Null,
Start,
Running,
Paused,
Complete,
Dispose
}
}

10
Avalonia.Gif/Decoding/BlockTypes.cs

@ -0,0 +1,10 @@
namespace Avalonia.Gif.Decoding
{
internal enum BlockTypes
{
Empty = 0,
Extension = 0x21,
ImageDescriptor = 0x2C,
Trailer = 0x3B,
}
}

8
Avalonia.Gif/Decoding/ExtensionType.cs

@ -0,0 +1,8 @@
namespace Avalonia.Gif.Decoding
{
internal enum ExtensionType
{
GraphicsControl = 0xF9,
Application = 0xFF
}
}

10
Avalonia.Gif/Decoding/FrameDisposal.cs

@ -0,0 +1,10 @@
namespace Avalonia.Gif.Decoding
{
public enum FrameDisposal
{
Unknown = 0,
Leave = 1,
Background = 2,
Restore = 3
}
}

36
Avalonia.Gif/Decoding/GifColor.cs

@ -0,0 +1,36 @@
using System.Runtime.InteropServices;
namespace Avalonia.Gif
{
[StructLayout(LayoutKind.Explicit)]
public readonly struct GifColor
{
[FieldOffset(3)]
public readonly byte A;
[FieldOffset(2)]
public readonly byte R;
[FieldOffset(1)]
public readonly byte G;
[FieldOffset(0)]
public readonly byte B;
/// <summary>
/// A struct that represents a ARGB color and is aligned as
/// a BGRA bytefield in memory.
/// </summary>
/// <param name="r">Red</param>
/// <param name="g">Green</param>
/// <param name="b">Blue</param>
/// <param name="a">Alpha</param>
public GifColor(byte r, byte g, byte b, byte a = byte.MaxValue)
{
A = a;
R = r;
G = g;
B = b;
}
}
}

653
Avalonia.Gif/Decoding/GifDecoder.cs

@ -0,0 +1,653 @@
// This source file's Lempel-Ziv-Welch algorithm is derived from Chromium's Android GifPlayer
// as seen here (https://github.com/chromium/chromium/blob/master/third_party/gif_player/src/jp/tomorrowkey/android/gifplayer)
// Licensed under the Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
// Copyright (C) 2015 The Gifplayer Authors. All Rights Reserved.
// The rest of the source file is licensed under MIT License.
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Avalonia;
using Avalonia.Media.Imaging;
using static Avalonia.Gif.Extensions.StreamExtensions;
namespace Avalonia.Gif.Decoding
{
public sealed class GifDecoder : IDisposable
{
private static readonly ReadOnlyMemory<byte> G87AMagic = "GIF87a"u8.ToArray().AsMemory();
private static readonly ReadOnlyMemory<byte> G89AMagic = "GIF89a"u8.ToArray().AsMemory();
private static readonly ReadOnlyMemory<byte> NetscapeMagic = "NETSCAPE2.0"u8.ToArray().AsMemory();
private static readonly TimeSpan FrameDelayThreshold = TimeSpan.FromMilliseconds(10);
private static readonly TimeSpan FrameDelayDefault = TimeSpan.FromMilliseconds(100);
private static readonly GifColor TransparentColor = new(0, 0, 0, 0);
private static readonly int MaxTempBuf = 768;
private static readonly int MaxStackSize = 4096;
private static readonly int MaxBits = 4097;
private readonly Stream _fileStream;
private readonly CancellationToken _currentCtsToken;
private readonly bool _hasFrameBackups;
private int _gctSize,
_bgIndex,
_prevFrame = -1,
_backupFrame = -1;
private bool _gctUsed;
private GifRect _gifDimensions;
// private ulong _globalColorTable;
private readonly int _backBufferBytes;
private GifColor[] _bitmapBackBuffer;
private short[] _prefixBuf;
private byte[] _suffixBuf;
private byte[] _pixelStack;
private byte[] _indexBuf;
private byte[] _backupFrameIndexBuf;
private volatile bool _hasNewFrame;
public GifHeader Header { get; private set; }
public readonly List<GifFrame> Frames = new();
public PixelSize Size => new PixelSize(Header.Dimensions.Width, Header.Dimensions.Height);
public GifDecoder(Stream fileStream, CancellationToken currentCtsToken)
{
_fileStream = fileStream;
_currentCtsToken = currentCtsToken;
ProcessHeaderData();
ProcessFrameData();
Header.IterationCount = Header.Iterations switch
{
-1 => new GifRepeatBehavior { Count = 1 },
0 => new GifRepeatBehavior { LoopForever = true },
> 0 => new GifRepeatBehavior { Count = Header.Iterations },
_ => Header.IterationCount
};
var pixelCount = _gifDimensions.TotalPixels;
_hasFrameBackups = Frames.Any(f => f.FrameDisposalMethod == FrameDisposal.Restore);
_bitmapBackBuffer = new GifColor[pixelCount];
_indexBuf = new byte[pixelCount];
if (_hasFrameBackups)
_backupFrameIndexBuf = new byte[pixelCount];
_prefixBuf = new short[MaxStackSize];
_suffixBuf = new byte[MaxStackSize];
_pixelStack = new byte[MaxStackSize + 1];
_backBufferBytes = pixelCount * Marshal.SizeOf(typeof(GifColor));
}
public void Dispose()
{
Frames.Clear();
_bitmapBackBuffer = null;
_prefixBuf = null;
_suffixBuf = null;
_pixelStack = null;
_indexBuf = null;
_backupFrameIndexBuf = null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int PixCoord(int x, int y) => x + y * _gifDimensions.Width;
static readonly (int Start, int Step)[] Pass = { (0, 8), (4, 8), (2, 4), (1, 2) };
private void ClearImage()
{
Array.Fill(_bitmapBackBuffer, TransparentColor);
//ClearArea(_gifDimensions);
_prevFrame = -1;
_backupFrame = -1;
}
public void RenderFrame(int fIndex, WriteableBitmap writeableBitmap, bool forceClear = false)
{
if (_currentCtsToken.IsCancellationRequested)
return;
if (fIndex < 0 | fIndex >= Frames.Count)
return;
if (_prevFrame == fIndex)
return;
if (fIndex == 0 || forceClear || fIndex < _prevFrame)
ClearImage();
DisposePreviousFrame();
_prevFrame++;
// render intermediate frame
for (int idx = _prevFrame; idx < fIndex; ++idx)
{
var prevFrame = Frames[idx];
if (prevFrame.FrameDisposalMethod == FrameDisposal.Restore)
continue;
if (prevFrame.FrameDisposalMethod == FrameDisposal.Background)
{
ClearArea(prevFrame.Dimensions);
continue;
}
RenderFrameAt(idx, writeableBitmap);
}
RenderFrameAt(fIndex, writeableBitmap);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RenderFrameAt(int idx, WriteableBitmap writeableBitmap)
{
var tmpB = ArrayPool<byte>.Shared.Rent(MaxTempBuf);
var curFrame = Frames[idx];
DecompressFrameToIndexBuffer(curFrame, _indexBuf, tmpB);
if (_hasFrameBackups & curFrame.ShouldBackup)
{
Buffer.BlockCopy(_indexBuf, 0, _backupFrameIndexBuf, 0, curFrame.Dimensions.TotalPixels);
_backupFrame = idx;
}
DrawFrame(curFrame, _indexBuf);
_prevFrame = idx;
_hasNewFrame = true;
using var lockedBitmap = writeableBitmap.Lock();
WriteBackBufToFb(lockedBitmap.Address);
ArrayPool<byte>.Shared.Return(tmpB);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DrawFrame(GifFrame curFrame, Memory<byte> frameIndexSpan)
{
var activeColorTable = curFrame.IsLocalColorTableUsed ? curFrame.LocalColorTable : Header.GlobarColorTable;
var cX = curFrame.Dimensions.X;
var cY = curFrame.Dimensions.Y;
var cH = curFrame.Dimensions.Height;
var cW = curFrame.Dimensions.Width;
var tC = curFrame.TransparentColorIndex;
var hT = curFrame.HasTransparency;
if (curFrame.IsInterlaced)
{
for (var i = 0; i < 4; i++)
{
var curPass = Pass[i];
var y = curPass.Start;
while (y < cH)
{
DrawRow(y);
y += curPass.Step;
}
}
}
else
{
for (var i = 0; i < cH; i++)
DrawRow(i);
}
//for (var row = 0; row < cH; row++)
void DrawRow(int row)
{
// Get the starting point of the current row on frame's index stream.
var indexOffset = row * cW;
// Get the target backbuffer offset from the frames coords.
var targetOffset = PixCoord(cX, row + cY);
var len = _bitmapBackBuffer.Length;
for (var i = 0; i < cW; i++)
{
var indexColor = frameIndexSpan.Span[indexOffset + i];
if (activeColorTable == null || targetOffset >= len || indexColor > activeColorTable.Length)
return;
if (!(hT & indexColor == tC))
_bitmapBackBuffer[targetOffset] = activeColorTable[indexColor];
targetOffset++;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DisposePreviousFrame()
{
if (_prevFrame == -1)
return;
var prevFrame = Frames[_prevFrame];
switch (prevFrame.FrameDisposalMethod)
{
case FrameDisposal.Background:
ClearArea(prevFrame.Dimensions);
break;
case FrameDisposal.Restore:
if (_hasFrameBackups && _backupFrame != -1)
DrawFrame(Frames[_backupFrame], _backupFrameIndexBuf);
else
ClearArea(prevFrame.Dimensions);
break;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ClearArea(GifRect area)
{
for (var y = 0; y < area.Height; y++)
{
var targetOffset = PixCoord(area.X, y + area.Y);
for (var x = 0; x < area.Width; x++)
_bitmapBackBuffer[targetOffset + x] = TransparentColor;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DecompressFrameToIndexBuffer(GifFrame curFrame, Span<byte> indexSpan, byte[] tempBuf)
{
_fileStream.Position = curFrame.LzwStreamPosition;
var totalPixels = curFrame.Dimensions.TotalPixels;
// Initialize GIF data stream decoder.
var dataSize = curFrame.LzwMinCodeSize;
var clear = 1 << dataSize;
var endOfInformation = clear + 1;
var available = clear + 2;
var oldCode = -1;
var codeSize = dataSize + 1;
var codeMask = (1 << codeSize) - 1;
for (var code = 0; code < clear; code++)
{
_prefixBuf[code] = 0;
_suffixBuf[code] = (byte)code;
}
// Decode GIF pixel stream.
int bits,
first,
top,
pixelIndex;
var datum = bits = first = top = pixelIndex = 0;
while (pixelIndex < totalPixels)
{
var blockSize = _fileStream.ReadBlock(tempBuf);
if (blockSize == 0)
break;
var blockPos = 0;
while (blockPos < blockSize)
{
datum += tempBuf[blockPos] << bits;
blockPos++;
bits += 8;
while (bits >= codeSize)
{
// Get the next code.
var code = datum & codeMask;
datum >>= codeSize;
bits -= codeSize;
// Interpret the code
if (code == clear)
{
// Reset decoder.
codeSize = dataSize + 1;
codeMask = (1 << codeSize) - 1;
available = clear + 2;
oldCode = -1;
continue;
}
// Check for explicit end-of-stream
if (code == endOfInformation)
return;
if (oldCode == -1)
{
indexSpan[pixelIndex++] = _suffixBuf[code];
oldCode = code;
first = code;
continue;
}
var inCode = code;
if (code >= available)
{
_pixelStack[top++] = (byte)first;
code = oldCode;
if (top == MaxBits)
ThrowException();
}
while (code >= clear)
{
if (code >= MaxBits || code == _prefixBuf[code])
ThrowException();
_pixelStack[top++] = _suffixBuf[code];
code = _prefixBuf[code];
if (top == MaxBits)
ThrowException();
}
first = _suffixBuf[code];
_pixelStack[top++] = (byte)first;
// Add new code to the dictionary
if (available < MaxStackSize)
{
_prefixBuf[available] = (short)oldCode;
_suffixBuf[available] = (byte)first;
available++;
if ((available & codeMask) == 0 && available < MaxStackSize)
{
codeSize++;
codeMask += available;
}
}
oldCode = inCode;
// Drain the pixel stack.
do
{
indexSpan[pixelIndex++] = _pixelStack[--top];
} while (top > 0);
}
}
}
while (pixelIndex < totalPixels)
indexSpan[pixelIndex++] = 0; // clear missing pixels
void ThrowException() => throw new LzwDecompressionException();
}
/// <summary>
/// Directly copies the <see cref="GifColor"/> struct array to a bitmap IntPtr.
/// </summary>
private void WriteBackBufToFb(IntPtr targetPointer)
{
if (_currentCtsToken.IsCancellationRequested)
return;
if (!(_hasNewFrame & _bitmapBackBuffer != null))
return;
unsafe
{
fixed (void* src = &_bitmapBackBuffer[0])
Buffer.MemoryCopy(src, targetPointer.ToPointer(), (uint)_backBufferBytes, (uint)_backBufferBytes);
_hasNewFrame = false;
}
}
/// <summary>
/// Processes GIF Header.
/// </summary>
private void ProcessHeaderData()
{
var str = _fileStream;
var tmpB = ArrayPool<byte>.Shared.Rent(MaxTempBuf);
var tempBuf = tmpB.AsSpan();
var _ = str.Read(tmpB, 0, 6);
if (!tempBuf[..3].SequenceEqual(G87AMagic[..3].Span))
throw new InvalidGifStreamException("Not a GIF stream.");
if (!(tempBuf[..6].SequenceEqual(G87AMagic.Span) | tempBuf[..6].SequenceEqual(G89AMagic.Span)))
throw new InvalidGifStreamException(
"Unsupported GIF Version: " + Encoding.ASCII.GetString(tempBuf[..6].ToArray())
);
ProcessScreenDescriptor(tmpB);
Header = new GifHeader
{
Dimensions = _gifDimensions,
HasGlobalColorTable = _gctUsed,
// GlobalColorTableCacheID = _globalColorTable,
GlobarColorTable = ProcessColorTable(ref str, tmpB, _gctSize),
GlobalColorTableSize = _gctSize,
BackgroundColorIndex = _bgIndex,
HeaderSize = _fileStream.Position
};
ArrayPool<byte>.Shared.Return(tmpB);
}
/// <summary>
/// Parses colors from file stream to target color table.
/// </summary>
private static GifColor[] ProcessColorTable(ref Stream stream, byte[] rawBufSpan, int nColors)
{
var nBytes = 3 * nColors;
var target = new GifColor[nColors];
var n = stream.Read(rawBufSpan, 0, nBytes);
if (n < nBytes)
throw new InvalidOperationException("Wrong color table bytes.");
int i = 0,
j = 0;
while (i < nColors)
{
var r = rawBufSpan[j++];
var g = rawBufSpan[j++];
var b = rawBufSpan[j++];
target[i++] = new GifColor(r, g, b);
}
return target;
}
/// <summary>
/// Parses screen and other GIF descriptors.
/// </summary>
private void ProcessScreenDescriptor(byte[] tempBuf)
{
var width = _fileStream.ReadUShortS(tempBuf);
var height = _fileStream.ReadUShortS(tempBuf);
var packed = _fileStream.ReadByteS(tempBuf);
_gctUsed = (packed & 0x80) != 0;
_gctSize = 2 << (packed & 7);
_bgIndex = _fileStream.ReadByteS(tempBuf);
_gifDimensions = new GifRect(0, 0, width, height);
_fileStream.Skip(1);
}
/// <summary>
/// Parses all frame data.
/// </summary>
private void ProcessFrameData()
{
_fileStream.Position = Header.HeaderSize;
var tempBuf = ArrayPool<byte>.Shared.Rent(MaxTempBuf);
var terminate = false;
var curFrame = 0;
Frames.Add(new GifFrame());
do
{
var blockType = (BlockTypes)_fileStream.ReadByteS(tempBuf);
switch (blockType)
{
case BlockTypes.Empty:
break;
case BlockTypes.Extension:
ProcessExtensions(ref curFrame, tempBuf);
break;
case BlockTypes.ImageDescriptor:
ProcessImageDescriptor(ref curFrame, tempBuf);
_fileStream.SkipBlocks(tempBuf);
break;
case BlockTypes.Trailer:
Frames.RemoveAt(Frames.Count - 1);
terminate = true;
break;
default:
_fileStream.SkipBlocks(tempBuf);
break;
}
// Break the loop when the stream is not valid anymore.
if (_fileStream.Position >= _fileStream.Length & terminate == false)
throw new InvalidProgramException("Reach the end of the filestream without trailer block.");
} while (!terminate);
ArrayPool<byte>.Shared.Return(tempBuf);
}
/// <summary>
/// Parses GIF Image Descriptor Block.
/// </summary>
private void ProcessImageDescriptor(ref int curFrame, byte[] tempBuf)
{
var str = _fileStream;
var currentFrame = Frames[curFrame];
// Parse frame dimensions.
var frameX = str.ReadUShortS(tempBuf);
var frameY = str.ReadUShortS(tempBuf);
var frameW = str.ReadUShortS(tempBuf);
var frameH = str.ReadUShortS(tempBuf);
frameW = (ushort)Math.Min(frameW, _gifDimensions.Width - frameX);
frameH = (ushort)Math.Min(frameH, _gifDimensions.Height - frameY);
currentFrame.Dimensions = new GifRect(frameX, frameY, frameW, frameH);
// Unpack interlace and lct info.
var packed = str.ReadByteS(tempBuf);
currentFrame.IsInterlaced = (packed & 0x40) != 0;
currentFrame.IsLocalColorTableUsed = (packed & 0x80) != 0;
currentFrame.LocalColorTableSize = (int)Math.Pow(2, (packed & 0x07) + 1);
if (currentFrame.IsLocalColorTableUsed)
currentFrame.LocalColorTable = ProcessColorTable(ref str, tempBuf, currentFrame.LocalColorTableSize);
currentFrame.LzwMinCodeSize = str.ReadByteS(tempBuf);
currentFrame.LzwStreamPosition = str.Position;
curFrame += 1;
Frames.Add(new GifFrame());
}
/// <summary>
/// Parses GIF Extension Blocks.
/// </summary>
private void ProcessExtensions(ref int curFrame, byte[] tempBuf)
{
var extType = (ExtensionType)_fileStream.ReadByteS(tempBuf);
switch (extType)
{
case ExtensionType.GraphicsControl:
_fileStream.ReadBlock(tempBuf);
var currentFrame = Frames[curFrame];
var packed = tempBuf[0];
currentFrame.FrameDisposalMethod = (FrameDisposal)((packed & 0x1c) >> 2);
if (
currentFrame.FrameDisposalMethod != FrameDisposal.Restore
&& currentFrame.FrameDisposalMethod != FrameDisposal.Background
)
currentFrame.ShouldBackup = true;
currentFrame.HasTransparency = (packed & 1) != 0;
currentFrame.FrameDelay = TimeSpan.FromMilliseconds(SpanToShort(tempBuf.AsSpan(1)) * 10);
if (currentFrame.FrameDelay <= FrameDelayThreshold)
currentFrame.FrameDelay = FrameDelayDefault;
currentFrame.TransparentColorIndex = tempBuf[3];
break;
case ExtensionType.Application:
var blockLen = _fileStream.ReadBlock(tempBuf);
var _ = tempBuf.AsSpan(0, blockLen);
var blockHeader = tempBuf.AsSpan(0, NetscapeMagic.Length);
if (blockHeader.SequenceEqual(NetscapeMagic.Span))
{
var count = 1;
while (count > 0)
count = _fileStream.ReadBlock(tempBuf);
var iterationCount = SpanToShort(tempBuf.AsSpan(1));
Header.Iterations = iterationCount;
}
else
_fileStream.SkipBlocks(tempBuf);
break;
default:
_fileStream.SkipBlocks(tempBuf);
break;
}
}
}
}

20
Avalonia.Gif/Decoding/GifFrame.cs

@ -0,0 +1,20 @@
using System;
namespace Avalonia.Gif.Decoding
{
public class GifFrame
{
public bool HasTransparency,
IsInterlaced,
IsLocalColorTableUsed;
public byte TransparentColorIndex;
public int LzwMinCodeSize,
LocalColorTableSize;
public long LzwStreamPosition;
public TimeSpan FrameDelay;
public FrameDisposal FrameDisposalMethod;
public bool ShouldBackup;
public GifRect Dimensions;
public GifColor[] LocalColorTable;
}
}

19
Avalonia.Gif/Decoding/GifHeader.cs

@ -0,0 +1,19 @@
// Licensed under the MIT License.
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
namespace Avalonia.Gif.Decoding
{
public class GifHeader
{
public bool HasGlobalColorTable;
public int GlobalColorTableSize;
public ulong GlobalColorTableCacheId;
public int BackgroundColorIndex;
public long HeaderSize;
internal int Iterations = -1;
public GifRepeatBehavior IterationCount;
public GifRect Dimensions;
private GifColor[] _globarColorTable;
public GifColor[] GlobarColorTable;
}
}

43
Avalonia.Gif/Decoding/GifRect.cs

@ -0,0 +1,43 @@
namespace Avalonia.Gif.Decoding
{
public readonly struct GifRect
{
public int X { get; }
public int Y { get; }
public int Width { get; }
public int Height { get; }
public int TotalPixels { get; }
public GifRect(int x, int y, int width, int height)
{
X = x;
Y = y;
Width = width;
Height = height;
TotalPixels = width * height;
}
public static bool operator ==(GifRect a, GifRect b)
{
return a.X == b.X && a.Y == b.Y && a.Width == b.Width && a.Height == b.Height;
}
public static bool operator !=(GifRect a, GifRect b)
{
return !(a == b);
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
return false;
return this == (GifRect)obj;
}
public override int GetHashCode()
{
return X.GetHashCode() ^ Y.GetHashCode() | Width.GetHashCode() ^ Height.GetHashCode();
}
}
}

8
Avalonia.Gif/Decoding/GifRepeatBehavior.cs

@ -0,0 +1,8 @@
namespace Avalonia.Gif.Decoding
{
public class GifRepeatBehavior
{
public bool LoopForever { get; set; }
public int? Count { get; set; }
}
}

23
Avalonia.Gif/Decoding/InvalidGifStreamException.cs

@ -0,0 +1,23 @@
// Licensed under the MIT License.
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
using System;
using System.Runtime.Serialization;
namespace Avalonia.Gif.Decoding
{
[Serializable]
public class InvalidGifStreamException : Exception
{
public InvalidGifStreamException() { }
public InvalidGifStreamException(string message)
: base(message) { }
public InvalidGifStreamException(string message, Exception innerException)
: base(message, innerException) { }
protected InvalidGifStreamException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}
}

23
Avalonia.Gif/Decoding/LzwDecompressionException.cs

@ -0,0 +1,23 @@
// Licensed under the MIT License.
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
using System;
using System.Runtime.Serialization;
namespace Avalonia.Gif.Decoding
{
[Serializable]
public class LzwDecompressionException : Exception
{
public LzwDecompressionException() { }
public LzwDecompressionException(string message)
: base(message) { }
public LzwDecompressionException(string message, Exception innerException)
: base(message, innerException) { }
protected LzwDecompressionException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}
}

81
Avalonia.Gif/Extensions/StreamExtensions.cs

@ -0,0 +1,81 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
namespace Avalonia.Gif.Extensions
{
[DebuggerStepThrough]
internal static class StreamExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort SpanToShort(Span<byte> b) => (ushort)(b[0] | (b[1] << 8));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Skip(this Stream stream, long count)
{
stream.Position += count;
}
/// <summary>
/// Read a Gif block from stream while advancing the position.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ReadBlock(this Stream stream, byte[] tempBuf)
{
stream.Read(tempBuf, 0, 1);
var blockLength = (int)tempBuf[0];
if (blockLength > 0)
stream.Read(tempBuf, 0, blockLength);
// Guard against infinite loop.
if (stream.Position >= stream.Length)
throw new InvalidGifStreamException("Reach the end of the filestream without trailer block.");
return blockLength;
}
/// <summary>
/// Skips GIF blocks until it encounters an empty block.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void SkipBlocks(this Stream stream, byte[] tempBuf)
{
int blockLength;
do
{
stream.Read(tempBuf, 0, 1);
blockLength = tempBuf[0];
stream.Position += blockLength;
// Guard against infinite loop.
if (stream.Position >= stream.Length)
throw new InvalidGifStreamException("Reach the end of the filestream without trailer block.");
} while (blockLength > 0);
}
/// <summary>
/// Read a <see cref="ushort"/> from stream by providing a temporary buffer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort ReadUShortS(this Stream stream, byte[] tempBuf)
{
stream.Read(tempBuf, 0, 2);
return SpanToShort(tempBuf);
}
/// <summary>
/// Read a <see cref="ushort"/> from stream by providing a temporary buffer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte ReadByteS(this Stream stream, byte[] tempBuf)
{
stream.Read(tempBuf, 0, 1);
var finalVal = tempBuf[0];
return finalVal;
}
}
}

297
Avalonia.Gif/GifImage.cs

@ -0,0 +1,297 @@
using System;
using System.IO;
using System.Numerics;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Rendering.Composition;
using Avalonia.VisualTree;
namespace Avalonia.Gif
{
public class GifImage : Control
{
public static readonly StyledProperty<string> SourceUriRawProperty = AvaloniaProperty.Register<
GifImage,
string
>("SourceUriRaw");
public static readonly StyledProperty<Uri> SourceUriProperty = AvaloniaProperty.Register<GifImage, Uri>(
"SourceUri"
);
public static readonly StyledProperty<Stream> SourceStreamProperty = AvaloniaProperty.Register<
GifImage,
Stream
>("SourceStream");
public static readonly StyledProperty<IterationCount> IterationCountProperty = AvaloniaProperty.Register<
GifImage,
IterationCount
>("IterationCount", IterationCount.Infinite);
private IGifInstance? _gifInstance;
public static readonly StyledProperty<StretchDirection> StretchDirectionProperty = AvaloniaProperty.Register<
GifImage,
StretchDirection
>("StretchDirection");
public static readonly StyledProperty<Stretch> StretchProperty = AvaloniaProperty.Register<GifImage, Stretch>(
"Stretch"
);
private CompositionCustomVisual? _customVisual;
private object? _initialSource = null;
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
switch (change.Property.Name)
{
case nameof(SourceUriRaw):
case nameof(SourceUri):
case nameof(SourceStream):
SourceChanged(change);
break;
case nameof(Stretch):
case nameof(StretchDirection):
InvalidateArrange();
InvalidateMeasure();
Update();
break;
case nameof(IterationCount):
IterationCountChanged(change);
break;
case nameof(Bounds):
Update();
break;
}
base.OnPropertyChanged(change);
}
public string SourceUriRaw
{
get => GetValue(SourceUriRawProperty);
set => SetValue(SourceUriRawProperty, value);
}
public Uri SourceUri
{
get => GetValue(SourceUriProperty);
set => SetValue(SourceUriProperty, value);
}
public Stream SourceStream
{
get => GetValue(SourceStreamProperty);
set => SetValue(SourceStreamProperty, value);
}
public IterationCount IterationCount
{
get => GetValue(IterationCountProperty);
set => SetValue(IterationCountProperty, value);
}
public StretchDirection StretchDirection
{
get => GetValue(StretchDirectionProperty);
set => SetValue(StretchDirectionProperty, value);
}
public Stretch Stretch
{
get => GetValue(StretchProperty);
set => SetValue(StretchProperty, value);
}
private static void IterationCountChanged(AvaloniaPropertyChangedEventArgs e)
{
var image = e.Sender as GifImage;
if (image is null || e.NewValue is not IterationCount iterationCount)
return;
image.IterationCount = iterationCount;
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
var compositor = ElementComposition.GetElementVisual(this)?.Compositor;
if (compositor == null || _customVisual?.Compositor == compositor)
return;
_customVisual = compositor.CreateCustomVisual(new CustomVisualHandler());
ElementComposition.SetElementChildVisual(this, _customVisual);
_customVisual.SendHandlerMessage(CustomVisualHandler.StartMessage);
if (_initialSource is not null)
{
UpdateGifInstance(_initialSource);
_initialSource = null;
}
Update();
base.OnAttachedToVisualTree(e);
}
private void Update()
{
if (_customVisual is null || _gifInstance is null)
return;
var dpi = this.GetVisualRoot()?.RenderScaling ?? 1.0;
var sourceSize = _gifInstance.GifPixelSize.ToSize(dpi);
var viewPort = new Rect(Bounds.Size);
var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection);
var scaledSize = sourceSize * scale;
var destRect = viewPort.CenterRect(new Rect(scaledSize)).Intersect(viewPort);
if (Stretch == Stretch.None)
{
_customVisual.Size = new Vector2((float)sourceSize.Width, (float)sourceSize.Height);
}
else
{
_customVisual.Size = new Vector2((float)destRect.Size.Width, (float)destRect.Size.Height);
}
_customVisual.Offset = new Vector3((float)destRect.Position.X, (float)destRect.Position.Y, 0);
}
private class CustomVisualHandler : CompositionCustomVisualHandler
{
private TimeSpan _animationElapsed;
private TimeSpan? _lastServerTime;
private IGifInstance? _currentInstance;
private bool _running;
public static readonly object StopMessage = new(),
StartMessage = new();
public override void OnMessage(object message)
{
if (message == StartMessage)
{
_running = true;
_lastServerTime = null;
RegisterForNextAnimationFrameUpdate();
}
else if (message == StopMessage)
{
_running = false;
}
else if (message is IGifInstance instance)
{
_currentInstance?.Dispose();
_currentInstance = instance;
}
}
public override void OnAnimationFrameUpdate()
{
if (!_running)
return;
Invalidate();
RegisterForNextAnimationFrameUpdate();
}
public override void OnRender(ImmediateDrawingContext drawingContext)
{
if (_running)
{
if (_lastServerTime.HasValue)
_animationElapsed += (CompositionNow - _lastServerTime.Value);
_lastServerTime = CompositionNow;
}
try
{
if (_currentInstance is null || _currentInstance.IsDisposed)
return;
var bitmap = _currentInstance.ProcessFrameTime(_animationElapsed);
if (bitmap is not null)
{
drawingContext.DrawBitmap(
bitmap,
new Rect(_currentInstance.GifPixelSize.ToSize(1)),
GetRenderBounds()
);
}
}
catch (Exception e)
{
Logger.Sink?.Log(LogEventLevel.Error, "GifImage Renderer ", this, e.ToString());
}
}
}
/// <summary>
/// Measures the control.
/// </summary>
/// <param name="availableSize">The available size.</param>
/// <returns>The desired size of the control.</returns>
protected override Size MeasureOverride(Size availableSize)
{
var result = new Size();
var scaling = this.GetVisualRoot()?.RenderScaling ?? 1.0;
if (_gifInstance != null)
{
result = Stretch.CalculateSize(
availableSize,
_gifInstance.GifPixelSize.ToSize(scaling),
StretchDirection
);
}
return result;
}
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
if (_gifInstance is null)
return new Size();
var scaling = this.GetVisualRoot()?.RenderScaling ?? 1.0;
var sourceSize = _gifInstance.GifPixelSize.ToSize(scaling);
var result = Stretch.CalculateSize(finalSize, sourceSize);
return result;
}
private void SourceChanged(AvaloniaPropertyChangedEventArgs e)
{
if (
e.NewValue is null
|| (e.NewValue is string value && !Uri.IsWellFormedUriString(value, UriKind.Absolute))
)
{
return;
}
if (_customVisual is null)
{
_initialSource = e.NewValue;
return;
}
UpdateGifInstance(e.NewValue);
InvalidateArrange();
InvalidateMeasure();
Update();
}
private void UpdateGifInstance(object source)
{
_gifInstance?.Dispose();
_gifInstance = new WebpInstance(source);
// _gifInstance = new GifInstance(source);
_gifInstance.IterationCount = IterationCount;
_customVisual?.SendHandlerMessage(_gifInstance);
}
}
}

147
Avalonia.Gif/GifInstance.cs

@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Gif.Decoding;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Avalonia.Gif
{
public class GifInstance : IGifInstance
{
public IterationCount IterationCount { get; set; }
public bool AutoStart { get; private set; } = true;
private readonly GifDecoder _gifDecoder;
private readonly WriteableBitmap? _targetBitmap;
private TimeSpan _totalTime;
private readonly List<TimeSpan> _frameTimes;
private uint _iterationCount;
private int _currentFrameIndex;
private readonly List<ulong> _colorTableIdList;
public CancellationTokenSource CurrentCts { get; }
internal GifInstance(object newValue)
: this(
newValue switch
{
Stream s => s,
Uri u => GetStreamFromUri(u),
string str => GetStreamFromString(str),
_ => throw new InvalidDataException("Unsupported source object")
}
) { }
public GifInstance(string uri)
: this(GetStreamFromString(uri)) { }
public GifInstance(Uri uri)
: this(GetStreamFromUri(uri)) { }
public GifInstance(Stream currentStream)
{
if (!currentStream.CanSeek)
throw new InvalidDataException("The provided stream is not seekable.");
if (!currentStream.CanRead)
throw new InvalidOperationException("Can't read the stream provided.");
currentStream.Seek(0, SeekOrigin.Begin);
CurrentCts = new CancellationTokenSource();
_gifDecoder = new GifDecoder(currentStream, CurrentCts.Token);
var pixSize = new PixelSize(_gifDecoder.Header.Dimensions.Width, _gifDecoder.Header.Dimensions.Height);
_targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque);
GifPixelSize = pixSize;
_totalTime = TimeSpan.Zero;
_frameTimes = _gifDecoder
.Frames
.Select(frame =>
{
_totalTime = _totalTime.Add(frame.FrameDelay);
return _totalTime;
})
.ToList();
_gifDecoder.RenderFrame(0, _targetBitmap);
}
private static Stream GetStreamFromString(string str)
{
if (!Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var res))
{
throw new InvalidCastException("The string provided can't be converted to URI.");
}
return GetStreamFromUri(res);
}
private static Stream GetStreamFromUri(Uri uri)
{
var uriString = uri.OriginalString.Trim();
if (!uriString.StartsWith("resm") && !uriString.StartsWith("avares"))
{
return new FileStream(uriString, FileMode.Open, FileAccess.Read);
}
return AssetLoader.Open(uri);
}
public int GifFrameCount => _frameTimes.Count;
public PixelSize GifPixelSize { get; }
public void Dispose()
{
IsDisposed = true;
CurrentCts.Cancel();
_targetBitmap?.Dispose();
}
public bool IsDisposed { get; private set; }
public WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed)
{
if (!IterationCount.IsInfinite && _iterationCount > IterationCount.Value)
{
return null;
}
if (CurrentCts.IsCancellationRequested || _targetBitmap is null)
{
return null;
}
var elapsedTicks = stopwatchElapsed.Ticks;
var timeModulus = TimeSpan.FromTicks(elapsedTicks % _totalTime.Ticks);
var targetFrame = _frameTimes.FirstOrDefault(x => timeModulus < x);
var currentFrame = _frameTimes.IndexOf(targetFrame);
if (currentFrame == -1)
currentFrame = 0;
if (_currentFrameIndex == currentFrame)
return _targetBitmap;
_iterationCount = (uint)(elapsedTicks / _totalTime.Ticks);
return ProcessFrameIndex(currentFrame);
}
internal WriteableBitmap ProcessFrameIndex(int frameIndex)
{
_gifDecoder.RenderFrame(frameIndex, _targetBitmap);
_currentFrameIndex = frameIndex;
return _targetBitmap;
}
}
}

15
Avalonia.Gif/IGifInstance.cs

@ -0,0 +1,15 @@
using Avalonia.Animation;
using Avalonia.Media.Imaging;
namespace Avalonia.Gif;
public interface IGifInstance : IDisposable
{
IterationCount IterationCount { get; set; }
bool AutoStart { get; }
CancellationTokenSource CurrentCts { get; }
int GifFrameCount { get; }
PixelSize GifPixelSize { get; }
bool IsDisposed { get; }
WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed);
}

20
Avalonia.Gif/InvalidGifStreamException.cs

@ -0,0 +1,20 @@
using System;
using System.Runtime.Serialization;
namespace Avalonia.Gif
{
[Serializable]
internal class InvalidGifStreamException : Exception
{
public InvalidGifStreamException() { }
public InvalidGifStreamException(string message)
: base(message) { }
public InvalidGifStreamException(string message, Exception innerException)
: base(message, innerException) { }
protected InvalidGifStreamException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}
}

180
Avalonia.Gif/WebpInstance.cs

@ -0,0 +1,180 @@
using Avalonia.Animation;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Gif;
public class WebpInstance : IGifInstance
{
public IterationCount IterationCount { get; set; }
public bool AutoStart { get; private set; } = true;
private readonly WriteableBitmap? _targetBitmap;
private TimeSpan _totalTime;
private readonly List<TimeSpan> _frameTimes;
private uint _iterationCount;
private int _currentFrameIndex;
private SKCodec? _codec;
public CancellationTokenSource CurrentCts { get; }
internal WebpInstance(object newValue)
: this(
newValue switch
{
Stream s => s,
Uri u => GetStreamFromUri(u),
string str => GetStreamFromString(str),
_ => throw new InvalidDataException("Unsupported source object")
}
) { }
public WebpInstance(string uri)
: this(GetStreamFromString(uri)) { }
public WebpInstance(Uri uri)
: this(GetStreamFromUri(uri)) { }
public WebpInstance(Stream currentStream)
{
if (!currentStream.CanSeek)
throw new InvalidDataException("The provided stream is not seekable.");
if (!currentStream.CanRead)
throw new InvalidOperationException("Can't read the stream provided.");
currentStream.Seek(0, SeekOrigin.Begin);
CurrentCts = new CancellationTokenSource();
var managedStream = new SKManagedStream(currentStream);
_codec = SKCodec.Create(managedStream);
var pixSize = new PixelSize(_codec.Info.Width, _codec.Info.Height);
_targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque);
GifPixelSize = pixSize;
_totalTime = TimeSpan.Zero;
_frameTimes = _codec
.FrameInfo
.Select(frame =>
{
_totalTime = _totalTime.Add(TimeSpan.FromMilliseconds(frame.Duration));
return _totalTime;
})
.ToList();
RenderFrame(_codec, _targetBitmap, 0);
}
private static void RenderFrame(SKCodec codec, WriteableBitmap targetBitmap, int index)
{
codec.GetFrameInfo(index, out var frameInfo);
var info = new SKImageInfo(codec.Info.Width, codec.Info.Height);
var decodeInfo = info.WithAlphaType(frameInfo.AlphaType);
using var frameBuffer = targetBitmap.Lock();
var result = codec.GetPixels(decodeInfo, frameBuffer.Address, new SKCodecOptions(index));
if (result != SKCodecResult.Success)
throw new InvalidDataException($"Could not decode frame {index} of {codec.FrameCount}.");
}
private static void RenderFrame(SKCodec codec, WriteableBitmap targetBitmap, int index, int priorIndex)
{
codec.GetFrameInfo(index, out var frameInfo);
var info = new SKImageInfo(codec.Info.Width, codec.Info.Height);
var decodeInfo = info.WithAlphaType(frameInfo.AlphaType);
using var frameBuffer = targetBitmap.Lock();
var result = codec.GetPixels(decodeInfo, frameBuffer.Address, new SKCodecOptions(index, priorIndex));
if (result != SKCodecResult.Success)
throw new InvalidDataException($"Could not decode frame {index} of {codec.FrameCount}.");
}
private static Stream GetStreamFromString(string str)
{
if (!Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var res))
{
throw new InvalidCastException("The string provided can't be converted to URI.");
}
return GetStreamFromUri(res);
}
private static Stream GetStreamFromUri(Uri uri)
{
var uriString = uri.OriginalString.Trim();
if (!uriString.StartsWith("resm") && !uriString.StartsWith("avares"))
{
return new FileStream(uriString, FileMode.Open, FileAccess.Read);
}
return AssetLoader.Open(uri);
}
public int GifFrameCount => _frameTimes.Count;
public PixelSize GifPixelSize { get; }
public void Dispose()
{
IsDisposed = true;
CurrentCts.Cancel();
_targetBitmap?.Dispose();
_codec?.Dispose();
}
public bool IsDisposed { get; private set; }
public WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed)
{
if (!IterationCount.IsInfinite && _iterationCount > IterationCount.Value)
{
return null;
}
if (CurrentCts.IsCancellationRequested || _targetBitmap is null)
{
return null;
}
var elapsedTicks = stopwatchElapsed.Ticks;
var timeModulus = TimeSpan.FromTicks(elapsedTicks % _totalTime.Ticks);
var targetFrame = _frameTimes.FirstOrDefault(x => timeModulus < x);
var currentFrame = _frameTimes.IndexOf(targetFrame);
if (currentFrame == -1)
currentFrame = 0;
if (_currentFrameIndex == currentFrame)
return _targetBitmap;
_iterationCount = (uint)(elapsedTicks / _totalTime.Ticks);
return ProcessFrameIndex(currentFrame);
}
internal WriteableBitmap ProcessFrameIndex(int frameIndex)
{
if (_codec is null)
throw new InvalidOperationException("The codec is null.");
if (_targetBitmap is null)
throw new InvalidOperationException("The target bitmap is null.");
RenderFrame(_codec, _targetBitmap, frameIndex, _currentFrameIndex);
_currentFrameIndex = frameIndex;
return _targetBitmap;
}
}

8
Build/AppEntitlements.entitlements

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>

12
Build/EmbeddedEntitlements.entitlements

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

26
Build/build_macos_app.sh

@ -0,0 +1,26 @@
#!/bin/sh
while getopts v: flag
do
case "${flag}" in
v) version=${OPTARG};;
*) echo "Invalid option";;
esac
done
dotnet \
msbuild \
StabilityMatrix.Avalonia \
-t:BundleApp \
-p:RuntimeIdentifier=osx-arm64 \
-p:UseAppHost=true \
-p:Configuration=Release \
-p:CFBundleShortVersionString="$version" \
-p:SelfContained=true \
-p:CFBundleName="Stability Matrix" \
-p:CFBundleDisplayName="Stability Matrix" \
-p:CFBundleVersion="$version" \
-p:PublishDir="$(pwd)/out/osx-arm64/bin" \
# Copy the app out of bin
cp -r ./out/osx-arm64/bin/Stability\ Matrix.app ./out/osx-arm64/Stability\ Matrix.app

62
Build/codesign_embedded_macos.sh

@ -0,0 +1,62 @@
#!/bin/sh
echo "Signing file: $1"
# Setup keychain in CI
if [ -n "$CI" ]; then
# Turn our base64-encoded certificate back to a regular .p12 file
echo "$MACOS_CERTIFICATE" | base64 --decode -o certificate.p12
# We need to create a new keychain, otherwise using the certificate will prompt
# with a UI dialog asking for the certificate password, which we can't
# use in a headless CI environment
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
fi
# Sign all files
PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" || return ; pwd -P )
ENTITLEMENTS="$PARENT_PATH/EmbeddedEntitlements.entitlements"
echo "Using entitlements file: $ENTITLEMENTS"
# App
if [ "$1" == "*.app" ]; then
echo "[INFO] Signing app contents"
find "$1/Contents/MacOS/"|while read fname; do
if [[ -f $fname ]]; then
echo "[INFO] Signing $fname"
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$fname"
fi
done
echo "[INFO] Signing app file"
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$1" -v
# Directory
elif [ -d "$1" ]; then
echo "[INFO] Signing directory contents"
find "$1"|while read fname; do
if [[ -f $fname ]] && [[ ! $fname =~ /(*.(py|msg|enc))/ ]]; then
echo "[INFO] Signing $fname"
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$fname"
fi
done
# File
elif [ -f "$1" ]; then
echo "[INFO] Signing file"
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$1" -v
# Not matched
else
echo "[ERROR] Unknown file type"
exit 1
fi

37
Build/codesign_macos.sh

@ -0,0 +1,37 @@
#!/bin/sh
echo "Signing file: $1"
# Setup keychain in CI
if [ -n "$CI" ]; then
# Turn our base64-encoded certificate back to a regular .p12 file
echo "$MACOS_CERTIFICATE" | base64 --decode -o certificate.p12
# We need to create a new keychain, otherwise using the certificate will prompt
# with a UI dialog asking for the certificate password, which we can't
# use in a headless CI environment
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
fi
# Sign all files
PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" || return ; pwd -P )
ENTITLEMENTS="$PARENT_PATH/AppEntitlements.entitlements"
echo "Using entitlements file: $ENTITLEMENTS"
find "$1/Contents/MacOS/"|while read fname; do
if [[ -f $fname ]]; then
echo "[INFO] Signing $fname"
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$fname"
fi
done
echo "[INFO] Signing app file"
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$1" -v

32
Build/notarize_macos.sh

@ -0,0 +1,32 @@
#!/bin/sh
echo "Notarizing file: $1"
# Store the notarization credentials so that we can prevent a UI password dialog
# from blocking the CI
echo "Create keychain profile"
xcrun notarytool store-credentials "notarytool-profile" \
--apple-id "$MACOS_NOTARIZATION_APPLE_ID" \
--team-id "$MACOS_NOTARIZATION_TEAM_ID" \
--password "$MACOS_NOTARIZATION_PWD"
# We can't notarize an app bundle directly, but we need to compress it as an archive.
# Therefore, we create a zip file containing our app bundle, so that we can send it to the
# notarization service
echo "Creating temp notarization archive"
ditto -c -k --keepParent "$1" "notarization.zip"
# Here we send the notarization request to the Apple's Notarization service, waiting for the result.
# This typically takes a few seconds inside a CI environment, but it might take more depending on the App
# characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
# you're curious
echo "Notarize app"
xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait
# Finally, we need to "attach the staple" to our executable, which will allow our app to be
# validated by macOS even when an internet connection is not available.
echo "Attach staple"
xcrun stapler staple "$1"

445
CHANGELOG.md

@ -5,6 +5,451 @@ 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
### Changed
- Changed package deletion warning dialog to require additional confirmation
### Fixed
- 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
## v2.9.0
### Added
- Added new package: [StableSwarmUI](https://github.com/Stability-AI/StableSwarmUI) by Stability AI
- Added new package: [Stable Diffusion WebUI Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) by lllyasviel
- Added extension management for SD.Next and Stable Diffusion WebUI-UX
- Added the ability to choose where CivitAI model downloads are saved
- Added `--launch-package` argument to launch a specific package on startup, using display name or package ID (i.e. `--launch-package "Stable Diffusion WebUI Forge"` or `--launch-package c0b3ecc5-9664-4be9-952d-a10b3dcaee14`)
- Added more Base Model search options to the CivitAI Model Browser
- Added Stable Cascade to the HuggingFace Model Browser
#### Inference
- Added Inference Prompt Styles, with Prompt Expansion model support (i.e. Fooocus V2)
- Added option to load a .yaml config file next to the model with the same name. Can be used with VPred and other models that require a config file.
- Added copy image support on linux and macOS for Inference outputs viewer menu
### Changed
- Updated translations for German, Spanish, French, Japanese, Portuguese, and Turkish
- (Internal) Updated to Avalonia 11.0.9
### Fixed
- Fixed StableSwarmUI not installing properly on macOS
- Fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS
- Fixed [#335](https://github.com/LykosAI/StabilityMatrix/issues/335) Update hanging indefinitely after git step for Auto1111 and SDWebUI Forge
- Fixed Inference output viewer menu "Copy" not copying image
- Fixed image viewer dialog arrow key navigation not working
- Fixed CivitAI login prompt not showing when downloading models that require CivitAI logins
- Fixed unknown model types not showing on checkpoints page (thanks Jerry!)
- Improved error handling for Inference Select Image hash calculation in case file is being written to while being read
## v2.9.0-pre.2
### Added
- Added `--launch-package` argument to launch a specific package on startup, using display name or package ID (i.e. `--launch-package "Stable Diffusion WebUI Forge"` or `--launch-package c0b3ecc5-9664-4be9-952d-a10b3dcaee14`)
- Added more Base Model search options to the CivitAI Model Browser
- Added Stable Cascade to the HuggingFace Model Browser
### Changed
- (Internal) Updated to Avalonia 11.0.9
### Fixed
- Fixed image viewer dialog arrow key navigation not working
- Fixed CivitAI login prompt not showing when downloading models that require CivitAI logins
## v2.9.0-pre.1
### Added
- Added Inference Prompt Styles, with Prompt Expansion model support (i.e. Fooocus 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
- Maybe fixed update hanging for Auto1111 and SDWebUI Forge
- Fixed Inference output viewer menu "Copy" not copying image
## v2.9.0-dev.2
### Added
#### Inference
- Added option to load a .yaml config file next to the model with the same name. Can be used with VPred and other models that require a config file.
### Fixed
- Fixed icon sizes of Inference Addons and Steps buttons
## v2.9.0-dev.1
### Added
- Added new package: [StableSwarmUI](https://github.com/Stability-AI/StableSwarmUI) by Stability AI
- Added new package: [Stable Diffusion WebUI Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) by lllyasviel
- Added extension management for SD.Next and Stable Diffusion WebUI-UX
- Added the ability to choose where CivitAI model downloads are saved
## v2.8.4
### Fixed
- Hopefully actually fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS
## v2.8.3
### Fixed
- Fixed user tokens read error causing failed downloads
- Failed downloads will now log error messages
- Fixed [#458](https://github.com/LykosAI/StabilityMatrix/issues/458) - Save Intermediate Image not working
- Fixed [#453](https://github.com/LykosAI/StabilityMatrix/issues/453) - Update Fooocus `--output-directory` argument to `--output-path`
## v2.8.2
### Added
- Added missing GFPGAN link to Automatic1111 packages
### Fixed
- Fixed Inference Image to Image Denoise setting becoming hidden after changing schedulers
- Fixed Inference ControlNet models showing as downloadable even when they are already installed
- Fixed Inference Sampler Addon conditioning not applying (i.e. ControlNet)
- Fixed extension modification dialog not showing any progress messages
## v2.8.1
### Fixed
- Fixed model links not working in RuinedFooocus for new installations
- Fixed incorrect nodejs download link on Linux (thanks to slogonomo for the fix)
- Fixed failing InvokeAI install on macOS due to missing nodejs
- Increased timeout on Recommended Models call to prevent potential timeout errors on slow connections
- Fixed SynchronizationLockException when saving settings
- Improved error messages with process output for 7z extraction errors
- Fixed missing tkinter dependency for OneTrainer on Windows
- Fixed auto-update on macOS not starting new version from an issue in starting .app bundles with arguments
- Fixed [#436](https://github.com/LykosAI/StabilityMatrix/issues/436) - Crash on invalid json files during checkpoint indexing
## v2.8.0
### Added
- Added Image to Video project type
- Added CLIP Skip setting to inference, toggleable from the model settings button
- Added image and model details in model selection boxes
- Added new package: [OneTrainer](https://github.com/Nerogar/OneTrainer)
- Added native desktop push notifications for some events (i.e. Downloads, Package installs, Inference generation)
- Currently available on Windows and Linux, macOS support is pending
- Added Package Extensions (Plugins) management - accessible from the Packages' 3-dot menu. Currently supports ComfyUI and Automatic1111.
- Added new launch argument options for Fooocus
- Added "Config" Shared Model Folder option for Fooocus
- Added Recommended Models dialog after one-click installer
- Added "Copy Details" button to Unexpected Error dialog
- Added German language option, thanks to Mario da Graca for the translation
- Added Portuguese language options, thanks to nextosai for the translation
- Added base model filter to Checkpoints page
- Added "Compatible Images" category when selecting images for Inference projects
- Added "Find in Model Browser" option to the right-click menu on the Checkpoints page
- Added `--use-directml` launch argument for SDWebUI DirectML fork
- Added release builds for macOS (Apple Silicon)
- Added ComfyUI launch argument configs: Cross Attention Method, Force Floating Point Precision, VAE Precision
- Added Delete button to the CivitAI Model Browser details dialog
- Added "Copy Link to Clipboard" for connected models in the Checkpoints page
- Added support for webp files to the Output Browser
- Added "Send to Image to Image" and "Send to Image to Video" options to the context menu
### Changed
- New package installation flow
- Changed one-click installer to match the new package installation style
- Automatic1111 packages will now use PyTorch v2.1.2. Upgrade will occur during the next package update or upon fresh installation.
- Search box on Checkpoints page now searches tags and trigger words
- Changed the Close button on the package install dialog to "Hide"
- Functionality remains the same, just a name change
- Updated translations for the following languages:
- Spanish
- French
- Japanese
- Turkish
- Inference file name patterns with directory separator characters will now have the subdirectories created automatically
- Changed how settings file is written to disk to reduce potential data loss risk
- (Internal) Updated to Avalonia 11.0.7
### Fixed
- Fixed error when ControlNet module image paths are not found, even if the module is disabled
- Fixed error when finding metadata for archived models
- Fixed error when extensions folder is missing
- Fixed crash when model was not selected in Inference
- Fixed Fooocus Config shared folder mode overwriting unknown config keys
- Fixed potential SD.Next update issues by moving to shared update process
- Fixed crash on startup when Outputs page failed to load categories properly
- Fixed image gallery arrow key navigation requiring clicking before responding
- Fixed crash when loading extensions list with no internet connection
- Fixed crash when invalid launch arguments are passed
- Fixed missing up/downgrade buttons on the Python Packages dialog when the version was not semver compatible
## v2.8.0-pre.5
### Fixed
- Fixed error when ControlNet module image paths are not found, even if the module is disabled
- Fixed error when finding metadata for archived models
- Fixed error when extensions folder is missing
- Fixed error when webp files have incorrect metadata
- Fixed crash when model was not selected in Inference
- Fixed Fooocus Config shared folder mode overwriting unknown config keys
## v2.8.0-pre.4
### Added
- Added Recommended Models dialog after one-click installer
- Added native desktop push notifications for some events (i.e. Downloads, Package installs, Inference generation)
- Currently available on Windows and Linux, macOS support is pending
- Added settings options for notifications
- Added new launch argument options for Fooocus
- Added Automatic1111 & Stable Diffusion WebUI-UX to the compatible macOS packages
### Changed
- Changed one-click installer to match the new package installation style
- Automatic1111 packages will now use PyTorch v2.1.2. Upgrade will occur during the next package update or upon fresh installation.
- Updated French translation with the latest changes
### Fixed
- Fixed [#413](https://github.com/LykosAI/StabilityMatrix/issues/413) - Environment Variables are editable again
- Fixed potential SD.Next update issues by moving to shared update process
- Fixed Invoke install trying to use system nodejs
- Fixed crash on startup when Outputs page failed to load categories properly
## v2.8.0-pre.3
### Added
- Added "Config" Shared Model Folder option for Fooocus
- Added "Copy Details" button to Unexpected Error dialog
### Changed
- (Internal) Updated to Avalonia 11.0.7
- Changed the Close button on the package install dialog to "Hide"
- Functionality remains the same, just a name change
- Updated French translation (thanks Greg!)
### Fixed
- Webp static images can now be shown alongside existing webp animation support
- Fixed image gallery arrow key navigation requiring clicking before responding
- Fixed crash when loading extensions list with no internet connection
- Fixed crash when invalid launch arguments are passed
- Fixed "must give at least one requirement to install" error when installing extensions with empty requirements.txt
## v2.8.0-pre.2
### Added
- Added German language option, thanks to Mario da Graca for the translation
- Added Portuguese language options, thanks to nextosai for the translation
### Changed
- Updated translations for the following languages:
- Spanish
- French
- Japanese
- Turkish
### Fixed
- Fixed Auto-update failing to start new version on Windows and Linux when path contains spaces
- Fixed InvokeAI v3.6.0 `"detail": "Not Found"` error when opening the UI
- Install button will now be properly disabled when the duplicate warning is shown
## v2.8.0-pre.1
### Added
- Added Package Extensions (Plugins) management - accessible from the Packages' 3-dot menu. Currently supports ComfyUI and A1111.
- Added base model filter to Checkpoints page
- Search box on Checkpoints page now searches tags and trigger words
- Added "Compatible Images" category when selecting images for Inference projects
- Added "Find in Model Browser" option to the right-click menu on the Checkpoints page
### Changed
- Removed "Failed to load image" notification when loading some images on the Checkpoints page
- Installed models will no longer be selectable on the Hugging Face tab of the model browser
### Fixed
- Inference file name patterns with directory separator characters will now have the subdirectories created automatically
- Fixed missing up/downgrade buttons on the Python Packages dialog when the version was not semver compatible
- Automatic1111 package installs will now install the missing `jsonmerge` package
## v2.8.0-dev.4
### Added
- Auto-update support for macOS
- New package installation flow
- Added `--use-directml` launch argument for SDWebUI DirectML fork
### Changed
- Changed default Period to "AllTime" in the Model Browser
### Fixed
- Fixed SDTurboScheduler's missing denoise parameter
## v2.8.0-dev.3
### Added
- Added release builds for macOS (Apple Silicon)
- Added new package: [OneTrainer](https://github.com/Nerogar/OneTrainer)
- Added ComfyUI launch argument configs: Cross Attention Method, Force Floating Point Precision, VAE Precision
- Added Delete button to the CivitAI Model Browser details dialog
- Added "Copy Link to Clipboard" for connected models in the Checkpoints page
### Changed
- Python Packages install dialog now allows entering multiple arguments or option flags
### Fixed
- Fixed environment variables grid not being editable related to [Avalonia #13843](https://github.com/AvaloniaUI/Avalonia/issues/13843)
## v2.8.0-dev.2
### Added
#### Inference
- Added Image to Video project type
#### Output Browser
- Added support for webp files
- Added "Send to Image to Image" and "Send to Image to Video" options to the context menu
### Changed
- Changed how settings file is written to disk to reduce potential data loss risk
## v2.8.0-dev.1
### Added
#### Inference
- Added image and model details in model selection boxes
- Added CLIP Skip setting, toggleable from the model settings button
## v2.7.9
### Fixed
- Fixed InvokeAI v3.6.0 `"detail": "Not Found"` error when opening the UI
## v2.7.8
### Changed
- Python Packages install dialog now allows entering multiple arguments or option flags
### Fixed
- Fixed InvokeAI Package dependency versions ([#395](https://github.com/LykosAI/StabilityMatrix/pull/395))
## v2.7.7
### Added
- Added `--use-directml` launch argument for SDWebUI DirectML fork

5
Directory.Build.props

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

76
README.md

@ -2,53 +2,58 @@
[![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-macos]: https://github.com/LykosAI/StabilityMatrix/issues/45
[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
[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
[voltaml]: https://github.com/VoltaML/voltaML-fast-stable-diffusion
[invokeai]: https://github.com/invoke-ai/InvokeAI
[fooocus]: https://github.com/lllyasviel/Fooocus
[fooocus-mre]: https://github.com/MoonRide303/Fooocus-MRE
[ruined-fooocus]: https://github.com/runew0lf/RuinedFooocus
[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], [Comfy UI][comfy], [SD.Next (Vladmandic)][sdnext], [VoltaML][voltaml], [InvokeAI][invokeai], [Fooocus][fooocus], and [Fooocus MRE][fooocus-mre]
- Supports:
- [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 ([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]
- 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-%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]
> macOS builds are currently pending: [#45][download-macos]
### 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
@ -62,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">
@ -106,6 +118,10 @@ Stability Matrix is now available in the following languages, thanks to our comm
- aolko
- 🇹🇷 Türkçe
- Progesor
- 🇩🇪 Deutsch
- Mario da Graca
- 🇵🇹 Português
- nextosai
If you would like to contribute a translation, please create an issue or contact us on Discord. Include an email where we'll send an invite to our [POEditor](https://poeditor.com/) project.

13
StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj

@ -19,12 +19,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.5" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="NLog" Version="5.2.5" />
<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.1" />
<PackageReference Include="NLog" Version="5.2.8" />
</ItemGroup>
<ItemGroup>

12
StabilityMatrix.Avalonia/App.axaml

@ -4,6 +4,8 @@
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. -->
@ -23,9 +25,11 @@
<ResourceInclude Source="Controls/Scroll/BetterScrollViewer.axaml"/>
<ResourceInclude Source="Styles/ControlThemes/HyperlinkIconButtonStyles.axaml"/>
<ResourceInclude Source="Styles/ControlThemes/ListBoxStyles.axaml"/>
<ResourceInclude Source="Styles/ControlThemes/BetterComboBoxStyles.axaml"/>
<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" />
@ -51,6 +55,8 @@
<StyleInclude Source="Styles/ToggleButtonStyles.axaml"/>
<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"/>
@ -58,6 +64,9 @@
<StyleInclude Source="Controls/SelectableImageCard/SelectableImageButton.axaml"/>
<StyleInclude Source="Controls/SettingsAccountLinkExpander.axaml"/>
<StyleInclude Source="Controls/StarsRating.axaml"/>
<StyleInclude Source="Controls/VideoGenerationSettingsCard.axaml"/>
<StyleInclude Source="Controls/VideoOutputSettingsCard.axaml"/>
<StyleInclude Source="Controls/TreeFileExplorer.axaml"/>
<StyleInclude Source="Controls/Inference/StackCard.axaml"/>
<StyleInclude Source="Controls/Inference/StackEditableCard.axaml"/>
<StyleInclude Source="Controls/Inference/StackExpander.axaml"/>
@ -72,6 +81,9 @@
<StyleInclude Source="Controls/Inference/SharpenCard.axaml"/>
<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}" />

240
StabilityMatrix.Avalonia/App.axaml.cs

@ -17,11 +17,13 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Input.Platform;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using Avalonia.Threading;
using FluentAvalonia.Interop;
using FluentAvalonia.UI.Controls;
using MessagePipe;
using Microsoft.Extensions.Configuration;
@ -38,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;
@ -52,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;
@ -80,7 +79,8 @@ public sealed class App : Application
public static TopLevel TopLevel => TopLevel.GetTopLevel(VisualRoot)!;
internal static bool IsHeadlessMode => TopLevel.TryGetPlatformHandle()?.HandleDescriptor is null or "STUB";
internal static bool IsHeadlessMode =>
TopLevel.TryGetPlatformHandle()?.HandleDescriptor is null or "STUB";
[NotNull]
public static IStorageProvider? StorageProvider { get; internal set; }
@ -96,6 +96,8 @@ public sealed class App : Application
public IClassicDesktopStyleApplicationLifetime? DesktopLifetime =>
ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
public static new App? Current => (App?)Application.Current;
/// <summary>
/// Called before <see cref="Services"/> is built.
/// Can be used by UI tests to override services.
@ -106,6 +108,8 @@ public sealed class App : Application
{
AvaloniaXamlLoader.Load(this);
SetFontFamily(GetPlatformDefaultFontFamily());
// Set design theme
if (Design.IsDesignMode)
{
@ -117,8 +121,7 @@ public sealed class App : Application
{
// Remove DataAnnotations validation plugin since we're using INotifyDataErrorInfo from MvvmToolkit
var dataValidationPluginsToRemove = BindingPlugins
.DataValidators
.OfType<DataAnnotationsValidationPlugin>()
.DataValidators.OfType<DataAnnotationsValidationPlugin>()
.ToArray();
foreach (var plugin in dataValidationPluginsToRemove)
@ -161,10 +164,7 @@ public sealed class App : Application
DesktopLifetime.MainWindow = setupWindow;
setupWindow
.ShowAsyncCts
.Token
.Register(() =>
setupWindow.ShowAsyncCts.Token.Register(() =>
{
if (setupWindow.Result == ContentDialogResult.Primary)
{
@ -185,6 +185,58 @@ public sealed class App : Application
}
}
/// <summary>
/// Set the default font family for the application.
/// </summary>
private void SetFontFamily(FontFamily fontFamily)
{
Resources["ContentControlThemeFontFamily"] = fontFamily;
}
/// <summary>
/// Get the default font family for the current platform and language.
/// </summary>
public FontFamily GetPlatformDefaultFontFamily()
{
try
{
var fonts = new List<string>();
if (Cultures.Current?.Name == "ja-JP")
{
return Resources["NotoSansJP"] as FontFamily
?? throw new ApplicationException("Font NotoSansJP not found");
}
if (Compat.IsWindows)
{
fonts.Add(OSVersionHelper.IsWindows11() ? "Segoe UI Variable Text" : "Segoe UI");
}
else if (Compat.IsMacOS)
{
// Use Segoe fonts if installed, but we can't distribute them
fonts.Add("Segoe UI Variable");
fonts.Add("Segoe UI");
fonts.Add("San Francisco");
fonts.Add("Helvetica Neue");
fonts.Add("Helvetica");
}
else
{
return FontFamily.Default;
}
return new FontFamily(string.Join(",", fonts));
}
catch (Exception e)
{
LogManager.GetCurrentClassLogger().Error(e);
return FontFamily.Default;
}
}
/// <summary>
/// Setup tasks to be run shortly before any window is shown
/// </summary>
@ -223,17 +275,13 @@ public sealed class App : Application
mainWindow.WindowStartupLocation = WindowStartupLocation.CenterScreen;
}
mainWindow.Closing += OnMainWindowClosing;
mainWindow.Closed += (_, _) => Shutdown();
mainWindow.SetDefaultFonts();
VisualRoot = mainWindow;
StorageProvider = mainWindow.StorageProvider;
Clipboard = mainWindow.Clipboard ?? throw new NullReferenceException("Clipboard is null");
DesktopLifetime.MainWindow = mainWindow;
DesktopLifetime.Exit += OnExit;
DesktopLifetime.ShutdownRequested += OnShutdownRequested;
}
private static void ConfigureServiceProvider()
@ -246,7 +294,10 @@ public sealed class App : Application
var settingsManager = Services.GetRequiredService<ISettingsManager>();
settingsManager.LibraryDirOverride = Program.Args.DataDirectoryOverride;
if (Program.Args.DataDirectoryOverride is not null)
{
settingsManager.SetLibraryDirOverride(Program.Args.DataDirectoryOverride);
}
if (settingsManager.TryFindLibrary())
{
@ -269,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<NewPackageManagerViewModel>(),
provider.GetRequiredService<InferenceViewModel>(),
provider.GetRequiredService<PackageManagerViewModel>(),
provider.GetRequiredService<CheckpointsPageViewModel>(),
provider.GetRequiredService<CheckpointBrowserViewModel>(),
provider.GetRequiredService<OutputsPageViewModel>()
provider.GetRequiredService<OutputsPageViewModel>(),
provider.GetRequiredService<WorkflowsPageViewModel>()
},
FooterPages = { provider.GetRequiredService<SettingsViewModel>() }
}
@ -297,7 +349,9 @@ public sealed class App : Application
var serviceManager = new ServiceManager<ViewModelBase>();
var serviceManagedTypes = exportedTypes
.Select(t => new { t, attributes = t.GetCustomAttributes(typeof(ManagedServiceAttribute), true) })
.Select(
t => new { t, attributes = t.GetCustomAttributes(typeof(ManagedServiceAttribute), true) }
)
.Where(t1 => t1.attributes is { Length: > 0 })
.Select(t1 => t1.t)
.ToList();
@ -322,8 +376,7 @@ public sealed class App : Application
services.AddMessagePipeNamedPipeInterprocess("StabilityMatrix");
var exportedTypes = AppDomain
.CurrentDomain
.GetAssemblies()
.CurrentDomain.GetAssemblies()
.Where(a => a.FullName?.StartsWith("StabilityMatrix") == true)
.SelectMany(a => a.GetExportedTypes())
.ToArray();
@ -332,7 +385,8 @@ public sealed class App : Application
.Select(t => new { t, attributes = t.GetCustomAttributes(typeof(TransientAttribute), false) })
.Where(
t1 =>
t1.attributes is { Length: > 0 } && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
t1.attributes is { Length: > 0 }
&& !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
)
.Select(t1 => new { Type = t1.t, Attribute = (TransientAttribute)t1.attributes[0] });
@ -352,9 +406,12 @@ public sealed class App : Application
.Select(t => new { t, attributes = t.GetCustomAttributes(typeof(SingletonAttribute), false) })
.Where(
t1 =>
t1.attributes is { Length: > 0 } && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
t1.attributes is { Length: > 0 }
&& !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
)
.Select(t1 => new { Type = t1.t, Attributes = t1.attributes.Cast<SingletonAttribute>().ToArray() });
.Select(
t1 => new { Type = t1.t, Attributes = t1.attributes.Cast<SingletonAttribute>().ToArray() }
);
foreach (var typePair in singletonTypes)
{
@ -386,7 +443,9 @@ public sealed class App : Application
// Rich presence
services.AddSingleton<IDiscordRichPresenceService, DiscordRichPresenceService>();
services.AddSingleton<IDisposable>(provider => provider.GetRequiredService<IDiscordRichPresenceService>());
services.AddSingleton<IDisposable>(
provider => provider.GetRequiredService<IDiscordRichPresenceService>()
);
Config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
@ -495,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);
@ -504,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);
@ -512,8 +571,8 @@ public sealed class App : Application
.AddRefitClient<ILykosAuthApi>(defaultRefitSettings)
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://stableauthentication.azurewebsites.net");
c.Timeout = TimeSpan.FromSeconds(15);
c.BaseAddress = new Uri("https://auth.lykos.ai");
c.Timeout = TimeSpan.FromSeconds(60);
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false })
.AddPolicyHandler(retryPolicy)
@ -522,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));
@ -557,7 +625,11 @@ public sealed class App : Application
#if DEBUG
builder.AddNLog(
ConfigureLogging(),
new NLogProviderOptions { IgnoreEmptyEventId = false, CaptureEventId = EventIdCaptureType.Legacy }
new NLogProviderOptions
{
IgnoreEmptyEventId = false,
CaptureEventId = EventIdCaptureType.Legacy
}
);
#else
builder.AddNLog(ConfigureLogging());
@ -577,42 +649,52 @@ public sealed class App : Application
{
if (Current is null)
throw new NullReferenceException("Current Application was null when Shutdown called");
if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
{
lifetime.Shutdown(exitCode);
try
{
var result = lifetime.TryShutdown(exitCode);
Debug.WriteLine($"Shutdown: {result}");
if (result)
{
Environment.Exit(exitCode);
}
}
/// <summary>
/// Handle shutdown requests (happens before <see cref="OnExit"/>)
/// </summary>
private static void OnMainWindowClosing(object? sender, WindowClosingEventArgs e)
catch (InvalidOperationException)
{
if (e.Cancel)
return;
var mainWindow = (MainWindow)sender!;
// Ignore in case already shutting down
}
}
else
{
Environment.Exit(exitCode);
}
}
// Show confirmation if package running
var launchPageViewModel = Services.GetRequiredService<LaunchPageViewModel>();
launchPageViewModel.OnMainWindowClosing(e);
private static void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e)
{
Debug.WriteLine("Start OnShutdownRequested");
if (e.Cancel)
return;
// Check if we need to dispose IAsyncDisposables
if (
!isAsyncDisposeComplete
&& Services.GetServices<IAsyncDisposable>().ToList() is { Count: > 0 } asyncDisposables
isAsyncDisposeComplete
|| Services.GetServices<IAsyncDisposable>().ToList() is not { Count: > 0 } asyncDisposables
)
{
return;
// Cancel shutdown for now
e.Cancel = true;
isAsyncDisposeComplete = true;
Debug.WriteLine("OnShutdownRequested Canceled: Disposing IAsyncDisposables");
Task.Run(async () =>
Dispatcher
.UIThread.InvokeAsync(async () =>
{
foreach (var disposable in asyncDisposables)
{
@ -630,38 +712,16 @@ public sealed class App : Application
.ContinueWith(_ =>
{
// Shutdown again
Dispatcher.UIThread.Invoke(() => Shutdown());
})
.SafeFireAndForget();
return;
}
OnMainWindowClosingTerminal(mainWindow);
}
Debug.WriteLine("Finished disposing IAsyncDisposables, shutting down");
/// <summary>
/// Called at the end of <see cref="OnMainWindowClosing"/> before the main window is closed.
/// </summary>
private static void OnMainWindowClosingTerminal(Window sender)
if (Dispatcher.UIThread.SupportsRunLoops)
{
var settingsManager = Services.GetRequiredService<ISettingsManager>();
// Save window position
var validWindowPosition = sender.Screens.All.Any(screen => screen.Bounds.Contains(sender.Position));
Dispatcher.UIThread.Invoke(() => Shutdown());
}
settingsManager.Transaction(
s =>
{
s.WindowSettings = new WindowSettings(
sender.Width,
sender.Height,
validWindowPosition ? sender.Position.X : 0,
validWindowPosition ? sender.Position.Y : 0
);
},
ignoreMissingLibraryDir: true
);
Environment.Exit(0);
})
.SafeFireAndForget();
}
private static void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs args)
@ -714,10 +774,12 @@ public sealed class App : Application
.WriteTo(
new FileTarget
{
Layout = "${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}",
Layout =
"${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}",
ArchiveOldFileOnStartup = true,
FileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.log",
ArchiveFileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log",
ArchiveFileName =
"${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log",
ArchiveNumbering = ArchiveNumberingMode.Rolling,
MaxArchiveFiles = 2
}
@ -730,7 +792,9 @@ public sealed class App : Application
builder.ForLogger("Microsoft.Extensions.Http.*").WriteToNil(NLog.LogLevel.Warn);
// Disable console trace logging by default
builder.ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel").WriteToNil(NLog.LogLevel.Debug);
builder
.ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel")
.WriteToNil(NLog.LogLevel.Debug);
// Disable LoadableViewModelBase trace logging by default
builder
@ -751,9 +815,7 @@ public sealed class App : Application
// Sentry
if (SentrySdk.IsEnabled)
{
LogManager
.Configuration
.AddSentry(o =>
LogManager.Configuration.AddSentry(o =>
{
o.InitializeSdk = false;
o.Layout = "${message}";
@ -803,12 +865,14 @@ public sealed class App : Application
results.Add(ms);
}
Dispatcher
.UIThread
.InvokeAsync(async () =>
Dispatcher.UIThread.InvokeAsync(async () =>
{
var dest = await StorageProvider.SaveFilePickerAsync(
new FilePickerSaveOptions() { SuggestedFileName = "screenshot.png", ShowOverwritePrompt = true }
new FilePickerSaveOptions()
{
SuggestedFileName = "screenshot.png",
ShowOverwritePrompt = true
}
);
if (dest?.TryGetLocalPath() is { } localPath)

28
StabilityMatrix.Avalonia/Assets.cs

@ -11,16 +11,19 @@ namespace StabilityMatrix.Avalonia;
internal static class Assets
{
public static AvaloniaResource AppIcon { get; } = new("avares://StabilityMatrix.Avalonia/Assets/Icon.ico");
public static AvaloniaResource AppIcon { get; } =
new("avares://StabilityMatrix.Avalonia/Assets/Icon.ico");
public static AvaloniaResource AppIconPng { get; } = new("avares://StabilityMatrix.Avalonia/Assets/Icon.png");
public static AvaloniaResource AppIconPng { get; } =
new("avares://StabilityMatrix.Avalonia/Assets/Icon.png");
/// <summary>
/// Fixed image for models with no images.
/// </summary>
public static Uri NoImage { get; } = new("avares://StabilityMatrix.Avalonia/Assets/noimage.png");
public static AvaloniaResource LicensesJson => new("avares://StabilityMatrix.Avalonia/Assets/licenses.json");
public static AvaloniaResource LicensesJson =>
new("avares://StabilityMatrix.Avalonia/Assets/licenses.json");
public static AvaloniaResource ImagePromptLanguageJson =>
new("avares://StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json");
@ -28,7 +31,8 @@ internal static class Assets
public static AvaloniaResource ThemeMatrixDarkJson =>
new("avares://StabilityMatrix.Avalonia/Assets/ThemeMatrixDark.json");
public static AvaloniaResource HfPackagesJson => new("avares://StabilityMatrix.Avalonia/Assets/hf-packages.json");
public static AvaloniaResource HfPackagesJson =>
new("avares://StabilityMatrix.Avalonia/Assets/hf-packages.json");
private const UnixFileMode Unix755 =
UnixFileMode.UserRead
@ -44,7 +48,10 @@ internal static class Assets
[SupportedOSPlatform("macos")]
public static AvaloniaResource SevenZipExecutable =>
Compat.Switch(
(PlatformKind.Windows, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe")),
(
PlatformKind.Windows,
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe")
),
(
PlatformKind.Linux | PlatformKind.X64,
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs", Unix755)
@ -112,10 +119,9 @@ internal static class Assets
PlatformKind.MacOS | PlatformKind.Arm,
new RemoteResource
{
Url = new Uri(
"https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11+20230507-aarch64-apple-darwin-install_only.tar.gz"
),
HashSha256 = "8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f"
// Requires our distribution with signed dylib for gatekeeper
Url = new Uri("https://cdn.lykos.ai/cpython-3.10.11-macos-arm64.zip"),
HashSha256 = "83c00486e0af9c460604a425e519d58e4b9604fbe7a4448efda0f648f86fb6e3"
}
)
);
@ -148,7 +154,9 @@ internal static class Assets
/// <summary>
/// Yield AvaloniaResources given a relative directory path within the 'Assets' folder.
/// </summary>
public static IEnumerable<(AvaloniaResource resource, string relativePath)> FindAssets(string relativeAssetPath)
public static IEnumerable<(AvaloniaResource resource, string relativePath)> FindAssets(
string relativeAssetPath
)
{
var baseUri = new Uri("avares://StabilityMatrix.Avalonia/Assets/");
var targetUri = new Uri(baseUri, relativeAssetPath);

BIN
StabilityMatrix.Avalonia/Assets/AppIcon.icns

Binary file not shown.

11
StabilityMatrix.Avalonia/Assets/hf-packages.json

@ -58,6 +58,17 @@
"LicenseType": "Stable-Video-Diffusion-NC-Community",
"LicensePath": "LICENSE"
},
{
"ModelCategory": "BaseModel",
"ModelName": "Stable Cascade",
"RepositoryPath": "stabilityai/stable-cascade",
"Files": [
"comfyui_checkpoints/stable_cascade_stage_b.safetensors",
"comfyui_checkpoints/stable_cascade_stage_c.safetensors"
],
"LicenseType": "stable-cascade-nc-community",
"LicensePath": "LICENSE"
},
{
"ModelCategory": "ControlNet",
"ModelName": "Canny",

14
StabilityMatrix.Avalonia/Assets/sitecustomize.py

@ -69,6 +69,20 @@ def _patch_rich_console():
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()
# Patch tqdm to use stdout instead of stderr

141
StabilityMatrix.Avalonia/Collections/SearchCollection.cs

@ -0,0 +1,141 @@
using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using DynamicData;
using DynamicData.Binding;
using JetBrains.Annotations;
namespace StabilityMatrix.Avalonia.Collections;
[PublicAPI]
public class SearchCollection<TObject, TKey, TQuery> : AbstractNotifyPropertyChanged, IDisposable
where TObject : notnull
where TKey : notnull
{
private readonly IDisposable cleanUp;
private Func<TQuery?, Func<TObject, bool>>? PredicateSelector { get; }
private Func<TQuery?, Func<TObject, (bool, int)>>? ScorerSelector { get; }
private Func<TObject, (bool, int)>? Scorer { get; set; }
private TQuery? _query;
public TQuery? Query
{
get => _query;
set => SetAndRaise(ref _query, value);
}
private SortExpressionComparer<TObject> _sortComparer = [];
public SortExpressionComparer<TObject> SortComparer
{
get => _sortComparer;
set => SetAndRaise(ref _sortComparer, value);
}
/// <summary>
/// Converts <see cref="SortComparer"/> to <see cref="SortExpressionComparer{SearchItem}"/>.
/// </summary>
private SortExpressionComparer<SearchItem> SearchItemSortComparer =>
[
..SortComparer
.Select(sortExpression => new SortExpression<SearchItem>(
item => sortExpression.Expression(item.Item),
sortExpression.Direction
)).Prepend(new SortExpression<SearchItem>(item => item.Score, SortDirection.Descending))
];
public IObservableCollection<TObject> Items { get; } = new ObservableCollectionExtended<TObject>();
public IObservableCollection<TObject> FilteredItems { get; } =
new ObservableCollectionExtended<TObject>();
public SearchCollection(
IObservable<IChangeSet<TObject, TKey>> source,
Func<TQuery?, Func<TObject, bool>> predicateSelector,
SortExpressionComparer<TObject>? sortComparer = null
)
{
PredicateSelector = predicateSelector;
if (sortComparer is not null)
{
SortComparer = sortComparer;
}
// Observable which creates a new predicate whenever Query property changes
var dynamicPredicate = this.WhenValueChanged(@this => @this.Query).Select(predicateSelector);
cleanUp = source
.Bind(Items)
.Filter(dynamicPredicate)
.Sort(SortComparer)
.Bind(FilteredItems)
.Subscribe();
}
public SearchCollection(
IObservable<IChangeSet<TObject, TKey>> source,
Func<TQuery?, Func<TObject, (bool, int)>> scorerSelector,
SortExpressionComparer<TObject>? sortComparer = null
)
{
ScorerSelector = scorerSelector;
if (sortComparer is not null)
{
SortComparer = sortComparer;
}
// Monitor Query property for changes
var queryChanged = this.WhenValueChanged(@this => @this.Query).Select(_ => Unit.Default);
cleanUp = new CompositeDisposable(
// Update Scorer property whenever Query property changes
queryChanged.Subscribe(_ => Scorer = scorerSelector(Query)),
// Transform source items into SearchItems
source
.Transform(
obj =>
{
var (isMatch, score) = Scorer?.Invoke(obj) ?? (true, 0);
return new SearchItem
{
Item = obj,
IsMatch = isMatch,
Score = score
};
},
forceTransform: queryChanged
)
.Filter(item => item.IsMatch)
.Sort(SearchItemSortComparer, SortOptimisations.ComparesImmutableValuesOnly)
.Transform(searchItem => searchItem.Item)
.Bind(FilteredItems)
.Subscribe()
);
}
/// <summary>
/// Clears <see cref="Query"/> property by setting it to default value.
/// </summary>
public void ClearQuery()
{
Query = default;
}
public void Dispose()
{
cleanUp.Dispose();
GC.SuppressFinalize(this);
}
private readonly record struct SearchItem
{
public TObject Item { get; init; }
public int Score { get; init; }
public bool IsMatch { get; init; }
}
}

35
StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml

@ -8,30 +8,53 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:gif="clr-namespace:Avalonia.Gif;assembly=Avalonia.Gif"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
d:DataContext="{x:Static mocks:DesignData.SampleImageSource}"
d:DesignHeight="450"
d:DesignWidth="800"
x:DataType="models:ImageSource"
mc:Ignorable="d">
<Grid>
<!-- Tag is not used but sets TemplateKey which is used to select the DataTemplate later -->
<ContentPresenter
Tag="{Binding TemplateKeyAsync^}"
Content="{Binding}">
<ContentPresenter.ContentTemplate>
<controls:DataTemplateSelector x:TypeArguments="models:ImageSourceTemplateType">
<DataTemplate x:Key="{x:Static models:ImageSourceTemplateType.WebpAnimation}" DataType="models:ImageSource">
<gif:GifImage
Stretch="Uniform"
SourceUri="{Binding LocalFile.FullPath}"/>
</DataTemplate>
<DataTemplate x:Key="{x:Static models:ImageSourceTemplateType.Image}" DataType="models:ImageSource">
<controls:AdvancedImageBox
Name="ImageBox"
CornerRadius="4"
Image="{Binding BitmapAsync^}"
SizeMode="Fit">
<controls:AdvancedImageBox.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem
x:Name="CopyMenuItem"
CommandParameter="{Binding #ImageBox.Image}"
Command="{x:Static controls:AdvancedImageBoxView.FlyoutCopyCommand}"
CommandParameter="{Binding}"
HotKey="Ctrl+C"
IconSource="Copy"
IsEnabled="{OnPlatform Windows=True,
Default=False}"
Text="Copy" />
Text="{x:Static lang:Resources.Action_Copy}" />
</ui:FAMenuFlyout>
</controls:AdvancedImageBox.ContextFlyout>
</controls:AdvancedImageBox>
</DataTemplate>
<DataTemplate x:Key="{x:Static models:ImageSourceTemplateType.Default}" DataType="models:ImageSource">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="Unsupported Format"/>
</DataTemplate>
</controls:DataTemplateSelector>
</ContentPresenter.ContentTemplate>
</ContentPresenter>
<!-- Label pill card -->
<Border

32
StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml.cs

@ -1,10 +1,9 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Helper;
namespace StabilityMatrix.Avalonia.Controls;
@ -16,25 +15,26 @@ public partial class AdvancedImageBoxView : UserControl
InitializeComponent();
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var copyMenuItem = this.FindControl<MenuFlyoutItem>("CopyMenuItem")!;
copyMenuItem.Command = new AsyncRelayCommand<Bitmap?>(FlyoutCopy);
}
public static AsyncRelayCommand<ImageSource?> FlyoutCopyCommand { get; } = new(FlyoutCopy);
private static async Task FlyoutCopy(Bitmap? image)
private static async Task FlyoutCopy(ImageSource? imageSource)
{
if (image is null || !Compat.IsWindows) return;
if (imageSource is null)
return;
if (Compat.IsWindows && imageSource.Bitmap is { } bitmap)
{
// Use bitmap on Windows if available
await Task.Run(() =>
{
if (Compat.IsWindows)
WindowsClipboard.SetBitmap(bitmap);
});
}
else if (imageSource.LocalFile is { } imagePath)
{
WindowsClipboard.SetBitmap(image);
// Other OS or no bitmap, use image source
var clipboard = App.Clipboard;
await clipboard.SetFileDataObjectAsync(imagePath);
}
});
}
}

38
StabilityMatrix.Avalonia/Controls/BetterComboBox.cs

@ -0,0 +1,38 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
namespace StabilityMatrix.Avalonia.Controls;
public class BetterComboBox : ComboBox
{
public static readonly DirectProperty<BetterComboBox, IDataTemplate?> SelectionBoxItemTemplateProperty =
AvaloniaProperty.RegisterDirect<BetterComboBox, IDataTemplate?>(
nameof(SelectionBoxItemTemplate),
v => v.SelectionBoxItemTemplate,
(x, v) => x.SelectionBoxItemTemplate = v
);
private IDataTemplate? _selectionBoxItemTemplate;
public IDataTemplate? SelectionBoxItemTemplate
{
get => _selectionBoxItemTemplate;
set => SetAndRaise(SelectionBoxItemTemplateProperty, ref _selectionBoxItemTemplate, value);
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (e.NameScope.Find<ContentControl>("ContentPresenter") is { } contentPresenter)
{
if (SelectionBoxItemTemplate is { } template)
{
contentPresenter.ContentTemplate = template;
}
}
}
}

62
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

@ -122,8 +122,10 @@ public class BetterContentDialog : ContentDialog
set => SetValue(ContentVerticalScrollBarVisibilityProperty, value);
}
public static readonly StyledProperty<double> MinDialogWidthProperty =
AvaloniaProperty.Register<BetterContentDialog, double>("MinDialogWidth");
public static readonly StyledProperty<double> MinDialogWidthProperty = AvaloniaProperty.Register<
BetterContentDialog,
double
>("MinDialogWidth");
public double MinDialogWidth
{
@ -131,8 +133,10 @@ public class BetterContentDialog : ContentDialog
set => SetValue(MinDialogWidthProperty, value);
}
public static readonly StyledProperty<double> MaxDialogWidthProperty =
AvaloniaProperty.Register<BetterContentDialog, double>("MaxDialogWidth");
public static readonly StyledProperty<double> MaxDialogWidthProperty = AvaloniaProperty.Register<
BetterContentDialog,
double
>("MaxDialogWidth");
public double MaxDialogWidth
{
@ -140,8 +144,21 @@ public class BetterContentDialog : ContentDialog
set => SetValue(MaxDialogWidthProperty, value);
}
public static readonly StyledProperty<double> MaxDialogHeightProperty =
AvaloniaProperty.Register<BetterContentDialog, double>("MaxDialogHeight");
public static readonly StyledProperty<double> MinDialogHeightProperty = AvaloniaProperty.Register<
BetterContentDialog,
double
>("MinDialogHeight");
public double MinDialogHeight
{
get => GetValue(MaxDialogHeightProperty);
set => SetValue(MaxDialogHeightProperty, value);
}
public static readonly StyledProperty<double> MaxDialogHeightProperty = AvaloniaProperty.Register<
BetterContentDialog,
double
>("MaxDialogHeight");
public double MaxDialogHeight
{
@ -149,8 +166,10 @@ public class BetterContentDialog : ContentDialog
set => SetValue(MaxDialogHeightProperty, value);
}
public static readonly StyledProperty<Thickness> ContentMarginProperty =
AvaloniaProperty.Register<BetterContentDialog, Thickness>("ContentMargin");
public static readonly StyledProperty<Thickness> ContentMarginProperty = AvaloniaProperty.Register<
BetterContentDialog,
Thickness
>("ContentMargin");
public Thickness ContentMargin
{
@ -158,8 +177,10 @@ public class BetterContentDialog : ContentDialog
set => SetValue(ContentMarginProperty, value);
}
public static readonly StyledProperty<bool> CloseOnClickOutsideProperty =
AvaloniaProperty.Register<BetterContentDialog, bool>("CloseOnClickOutside");
public static readonly StyledProperty<bool> CloseOnClickOutsideProperty = AvaloniaProperty.Register<
BetterContentDialog,
bool
>("CloseOnClickOutside");
/// <summary>
/// Whether to close the dialog when clicking outside of it (on the blurred background)
@ -187,13 +208,18 @@ public class BetterContentDialog : ContentDialog
var point = e.GetPosition(this);
if (
!backgroundPart.Bounds.Contains(point)
&& (Content as Control)?.DataContext is ContentDialogViewModelBase vm
)
if (!backgroundPart.Bounds.Contains(point))
{
// Use vm if available
if ((Content as Control)?.DataContext is ContentDialogViewModelBase vm)
{
vm.OnCloseButtonClick();
}
else
{
Hide(ContentDialogResult.None);
}
}
}
}
@ -211,10 +237,7 @@ public class BetterContentDialog : ContentDialog
viewModelDirect.SecondaryButtonClick += OnDialogButtonClick;
viewModelDirect.CloseButtonClick += OnDialogButtonClick;
}
else if (
(Content as Control)?.DataContext
is ContentDialogProgressViewModelBase progressViewModel
)
else if ((Content as Control)?.DataContext is ContentDialogProgressViewModelBase progressViewModel)
{
progressViewModel.PrimaryButtonClick += OnDialogButtonClick;
progressViewModel.SecondaryButtonClick += OnDialogButtonClick;
@ -234,8 +257,7 @@ public class BetterContentDialog : ContentDialog
}
else
{
PrimaryButton.IsVisible =
IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText);
PrimaryButton.IsVisible = IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText);
}
}

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;
}
}

46
StabilityMatrix.Avalonia/Controls/DataTemplateSelector.cs

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Metadata;
using JetBrains.Annotations;
using StabilityMatrix.Avalonia.Models;
namespace StabilityMatrix.Avalonia.Controls;
/// <summary>
/// Selector for objects implementing <see cref="ITemplateKey{T}"/>
/// </summary>
[PublicAPI]
public class DataTemplateSelector<TKey> : IDataTemplate
where TKey : notnull
{
/// <summary>
/// Key that is used when no other key matches
/// </summary>
public TKey? DefaultKey { get; set; }
[Content]
public Dictionary<TKey, IDataTemplate> Templates { get; } = new();
public bool Match(object? data) => data is ITemplateKey<TKey>;
/// <inheritdoc />
public Control Build(object? data)
{
if (data is not ITemplateKey<TKey> key)
throw new ArgumentException(null, nameof(data));
if (Templates.TryGetValue(key.TemplateKey, out var template))
{
return template.Build(data)!;
}
if (DefaultKey is not null && Templates.TryGetValue(DefaultKey, out var defaultTemplate))
{
return defaultTemplate.Build(data)!;
}
throw new ArgumentException(null, nameof(data));
}
}

113
StabilityMatrix.Avalonia/Controls/HyperlinkIconButton.cs

@ -1,13 +1,122 @@
using System;
using System.Diagnostics;
using System.IO;
using AsyncAwaitBestPractices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Logging;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Core.Processes;
using Symbol = FluentIcons.Common.Symbol;
namespace StabilityMatrix.Avalonia.Controls;
/// <summary>
/// Like <see cref="HyperlinkButton"/>, but with a link icon left of the text content.
/// </summary>
public class HyperlinkIconButton : HyperlinkButton
public class HyperlinkIconButton : Button
{
/// <inheritdoc />
private Uri? _navigateUri;
/// <summary>
/// Defines the <see cref="NavigateUri"/> property
/// </summary>
public static readonly DirectProperty<HyperlinkIconButton, Uri?> NavigateUriProperty =
AvaloniaProperty.RegisterDirect<HyperlinkIconButton, Uri?>(
nameof(NavigateUri),
x => x.NavigateUri,
(x, v) => x.NavigateUri = v
);
/// <summary>
/// Gets or sets the Uri that the button should navigate to upon clicking. In assembly paths are not supported, (e.g., avares://...)
/// </summary>
public Uri? NavigateUri
{
get => _navigateUri;
set => SetAndRaise(NavigateUriProperty, ref _navigateUri, value);
}
public static readonly StyledProperty<Symbol> IconProperty = AvaloniaProperty.Register<
HyperlinkIconButton,
Symbol
>("Icon", Symbol.Link);
public Symbol Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
protected override Type StyleKeyOverride => typeof(HyperlinkIconButton);
/// <inheritdoc />
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
// Update icon
if (change.Property == NavigateUriProperty)
{
var uri = change.GetNewValue<Uri?>();
if (uri is not null && uri.IsFile && Icon == Symbol.Link)
{
Icon = Symbol.Open;
}
}
}
protected override void OnClick()
{
base.OnClick();
if (NavigateUri is null)
return;
// File or Folder URIs
if (NavigateUri.IsFile)
{
var path = NavigateUri.LocalPath;
if (Directory.Exists(path))
{
ProcessRunner
.OpenFolderBrowser(path)
.SafeFireAndForget(ex =>
{
Logger.TryGet(LogEventLevel.Error, $"Unable to open directory Uri {NavigateUri}");
});
}
else if (File.Exists(path))
{
ProcessRunner
.OpenFileBrowser(path)
.SafeFireAndForget(ex =>
{
Logger.TryGet(LogEventLevel.Error, $"Unable to open file Uri {NavigateUri}");
});
}
}
// Web
else
{
try
{
Process.Start(
new ProcessStartInfo(NavigateUri.ToString()) { UseShellExecute = true, Verb = "open" }
);
}
catch
{
Logger.TryGet(LogEventLevel.Error, $"Unable to open Uri {NavigateUri}");
}
}
}
protected override bool RegisterContentPresenter(ContentPresenter presenter)
{
return presenter.Name == "ContentPresenter";
}
}

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

@ -2,7 +2,7 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
@ -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"
ColumnDefinitions="*,Auto">
<ui:FAComboBox
Margin="0,0,0,4"
SelectedItem="{Binding SelectedPreprocessor}"
ItemsSource="{Binding ClientManager.Preprocessors}"
DisplayMemberBinding="{Binding DisplayName}"
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}" />
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
@ -102,6 +119,45 @@
</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
ColumnDefinitions="*,Auto,Auto"

22
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>
@ -19,6 +22,8 @@
</Panel>
</Design.PreviewWith>
<converters:FileUriConverter x:Key="FileUriConverter" />
<ControlTheme x:Key="{x:Type controls:ImageFolderCard}" TargetType="controls:ImageFolderCard">
<Setter Property="Background" Value="Transparent" />
<!-- Set Defaults -->
@ -43,18 +48,12 @@
<Grid RowDefinitions="Auto,*">
<TextBox
x:Name="SearchBox"
Classes="search"
Margin="6,4,6,8"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Text="{Binding SearchQuery}"
Watermark="{x:Static lang:Resources.Label_SearchEllipsis}">
<TextBox.InnerRightContent>
<ui:SymbolIcon
Margin="0,0,6,0"
FontSize="16"
Symbol="Find" />
</TextBox.InnerRightContent>
</TextBox>
Watermark="{x:Static lang:Resources.Label_SearchEllipsis}"/>
<scroll:BetterScrollViewer
Grid.Row="1"
@ -201,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;

190
StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml

@ -1,146 +1,93 @@
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="https://github.com/projektanker/icons.avalonia"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:fluent="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="clr-namespace: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"
x:DataType="inference:ModelCardViewModel">
<Design.PreviewWith>
<Panel Width="400" Height="200">
<StackPanel Width="300" VerticalAlignment="Center">
<controls:ModelCard DataContext="{x:Static mocks:DesignData.ModelCardViewModel}" />
<controls:ModelCard DataContext="{x:Static mocks:DesignData.ImgToVidModelCardViewModel}" />
</StackPanel>
</Panel>
</Design.PreviewWith>
<Styles.Resources>
<fluent:SymbolIconSource
x:Key="IconCube"
IsFilled="False"
Symbol="Cube" />
<fluent:SymbolIconSource
x:Key="IconQuestionCircle"
IsFilled="True"
Symbol="QuestionCircle" />
<fluent:SymbolIconSource
x:Key="IconTableCellEdit"
IsFilled="True"
Symbol="TableCellEdit" />
</Styles.Resources>
<Style Selector="controls|ModelCard">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="12">
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="*,*,*">
<sg:SpacedGrid
ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="8"
RowDefinitions="*,*,*,*"
RowSpacing="0">
<!-- Model -->
<TextBlock
Grid.Column="0"
MinWidth="60"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Model}"
TextAlignment="Left" />
<ui:FAComboBox
<controls:BetterComboBox
Grid.Row="0"
Grid.Column="1"
Theme="{StaticResource FAComboBoxHybridModelTheme}"
Padding="8,6,4,6"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Models}"
SelectedItem="{Binding SelectedModel}">
<!--<ui:FAComboBox.Styles>
<Style Selector="ui|FAComboBoxItem">
<Setter Property="ToolTip.Placement" Value="RightEdgeAlignedTop" />
<Setter Property="ToolTip.Tip">
<Template>
<StackPanel
x:DataType="models:HybridModelFile"
Orientation="Horizontal"
Spacing="6">
~1~ Image @1@
<controls:BetterAdvancedImage
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" />
<Grid RowDefinitions="Auto,Auto,*" VerticalAlignment="Stretch" MaxWidth="300">
~1~ Title @1@
<TextBlock
Margin="0,0,0,4"
HorizontalAlignment="Left"
FontSize="14"
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.ModelName, FallbackValue=''}"
TextWrapping="WrapWithOverflow" />
~1~ Version @1@
<TextBlock
Grid.Row="1"
Margin="0,0,0,8"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.VersionName, FallbackValue=''}"
TextWrapping="WrapWithOverflow" />
~1~ Path @1@
<TextBlock
Grid.Row="2"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="{Binding RelativePath}"
TextWrapping="Wrap" />
</Grid>
</StackPanel>
</Template>
</Setter>
</Style>
<Style Selector="ui|FAComboBox /template/ ContentControl#ContentPresenter &gt; StackPanel &gt; TextBlock:nth-child(2)">
<Setter Property="IsVisible" Value="False" />
</Style>
</ui:FAComboBox.Styles>
<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="models:HybridModelFile">
<StackPanel ToolTip.Placement="RightEdgeAlignedTop">
<TextBlock Text="{Binding ShortDisplayName}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ui:FAComboBox.ItemTemplate>-->
<!--<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="models:HybridModelFile">
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*">
<controls:BetterAdvancedImage
Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2"
Margin="0,0,8,0"
Height="32"
Width="32"
Stretch="UniformToFill"
StretchDirection="Both"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal}">
<controls:BetterAdvancedImage.Clip>
<EllipseGeometry Rect="0,0,32,32" />
</controls:BetterAdvancedImage.Clip>
</controls:BetterAdvancedImage>
<TextBlock
Grid.Row="0"
Grid.Column="1"
Text="{Binding Local.ConnectedModelInfo.VersionName}"/>
<TextBlock
FontSize="11"
Grid.Row="1"
Grid.Column="1"
Text="{Binding Local.ConnectedModelInfo.ModelName}"/>
</Grid>
</DataTemplate>
</ui:FAComboBox.ItemTemplate>-->
</ui:FAComboBox>
SelectedItem="{Binding SelectedModel}"
Theme="{StaticResource BetterComboBoxHybridModelTheme}" />
<Button
Grid.Row="0"
Grid.Column="2"
Margin="8,0,0,0">
Margin="0,0,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<ui:SymbolIcon FontSize="16" Symbol="Setting" />
<Button.Flyout>
<ui:FAMenuFlyout Placement="BottomEdgeAlignedLeft">
<ui:ToggleMenuFlyoutItem IsChecked="{Binding IsRefinerSelectionEnabled}" Text="{x:Static lang:Resources.Label_Refiner}" />
<ui:ToggleMenuFlyoutItem IsChecked="{Binding IsVaeSelectionEnabled}" Text="{x:Static lang:Resources.Label_VAE}" />
<ui:ToggleMenuFlyoutItem
IconSource="{StaticResource IconCube}"
IsChecked="{Binding IsRefinerSelectionEnabled}"
Text="{x:Static lang:Resources.Label_Refiner}" />
<ui:ToggleMenuFlyoutItem
IconSource="{StaticResource IconCube}"
IsChecked="{Binding IsVaeSelectionEnabled}"
Text="{x:Static lang:Resources.Label_VAE}" />
<ui:ToggleMenuFlyoutItem
IconSource="{StaticResource IconTableCellEdit}"
IsChecked="{Binding IsClipSkipEnabled}"
Text="{x:Static lang:Resources.Label_CLIPSkip}" />
<ui:MenuFlyoutSeparator />
<ui:MenuFlyoutItem
Command="{Binding ConfigClickCommand}"
IconSource="{StaticResource IconQuestionCircle}"
Text="{x:Static lang:Resources.Label_Config}" />
</ui:FAMenuFlyout>
</Button.Flyout>
</Button>
@ -149,30 +96,28 @@
<TextBlock
Grid.Row="1"
Grid.Column="0"
MinWidth="60"
Margin="0,8,0,0"
VerticalAlignment="Center"
IsVisible="{Binding IsRefinerSelectionEnabled}"
Text="{x:Static lang:Resources.Label_Refiner}"
TextAlignment="Left" />
<ui:FAComboBox
<controls:BetterComboBox
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="0,8,0,0"
Padding="8,6,4,6"
HorizontalAlignment="Stretch"
DisplayMemberBinding="{Binding ShortDisplayName}"
IsTextSearchEnabled="True"
IsVisible="{Binding IsRefinerSelectionEnabled}"
ItemsSource="{Binding ClientManager.Models}"
SelectedItem="{Binding SelectedRefiner}" />
SelectedItem="{Binding SelectedRefiner}"
Theme="{StaticResource BetterComboBoxHybridModelTheme}" />
<!-- VAE -->
<TextBlock
Grid.Row="2"
Grid.Column="0"
MinWidth="60"
Margin="0,8,0,0"
VerticalAlignment="Center"
IsVisible="{Binding IsVaeSelectionEnabled}"
@ -191,7 +136,32 @@
ItemsSource="{Binding ClientManager.VaeModels}"
SelectedItem="{Binding SelectedVae}" />
</Grid>
<!-- CLIP Skip -->
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="0,8,0,0"
VerticalAlignment="Center"
IsVisible="{Binding IsClipSkipEnabled}"
Text="{x:Static lang:Resources.Label_CLIPSkip}"
TextAlignment="Left" />
<NumericUpDown
Grid.Row="3"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
ClipValueToMinMax="True"
Increment="1"
IsVisible="{Binding IsClipSkipEnabled}"
Maximum="24"
Minimum="1"
ParsingNumberStyle="Integer"
Watermark="1"
Value="{Binding ClipSkip, Converter={x:Static converters:NullableDefaultNumericConverters.IntToDecimal}}" />
</sg:SpacedGrid>
</controls:Card>
</ControlTemplate>
</Setter>

60
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">
@ -33,7 +35,7 @@
</Style>
</controls:Card.Styles>
<Grid RowDefinitions="*,16,*">
<Grid RowDefinitions="*,16,*,16,Auto">
<!-- Prompt -->
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*">
<StackPanel
@ -56,7 +58,9 @@
HorizontalAlignment="Right"
Orientation="Horizontal">
<Panel>
<Button
x:Name="PART_HelpButton"
Margin="0,-2,0,0"
Padding="10,4"
VerticalAlignment="Top"
@ -65,6 +69,12 @@
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}"
@ -137,6 +147,54 @@
</Border>
</Grid>
<GridSplitter
Grid.Row="3"
MaxWidth="45"
VerticalAlignment="Center"
BorderThickness="1"
CornerRadius="4"
Opacity="0.3" />
<controls:StackEditableCard
Margin="2,0,0,0"
DataContext="{Binding ModulesCardViewModel}"
Grid.Row="4">
</controls:StackEditableCard>
<!-- Styles and Prompt Expansions -->
<!--<Grid Grid.Row="4" RowDefinitions="Auto,*">
<StackPanel Margin="4,0,4,8" Orientation="Horizontal">
<TextBlock FontSize="14" Text="Styles" />
<icons:Icon
Margin="8,0"
FontSize="10"
Value="fa-solid fa-caret-down" />
</StackPanel>
<Border
Grid.Row="1"
Classes="theme-dark"
VerticalAlignment="Stretch"
CornerRadius="4">
<avaloniaEdit:TextEditor
x:Name="ExtraPromptEditor"
Document="{Binding NegativePromptDocument}"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace">
<i:Interaction.Behaviors>
<behaviors:TextEditorCompletionBehavior
CompletionProvider="{Binding CompletionProvider}"
IsEnabled="{Binding IsAutoCompletionEnabled}"
TokenizerProvider="{Binding TokenizerProvider}" />
<behaviors:TextEditorToolTipBehavior IsEnabled="False" TokenizerProvider="{Binding TokenizerProvider}" />
</i:Interaction.Behaviors>
</avaloniaEdit:TextEditor>
</Border>
</Grid>-->
</Grid>
</controls:Card>
</ControlTemplate>

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)

96
StabilityMatrix.Avalonia/Controls/Inference/PromptExpansionCard.axaml

@ -0,0 +1,96 @@
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="clr-namespace: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:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:local="clr-namespace:StabilityMatrix.Avalonia"
x:DataType="inference:PromptExpansionCardViewModel">
<Design.PreviewWith>
<Panel Width="400" Height="200">
<StackPanel Width="300" VerticalAlignment="Center">
<controls:PromptExpansionCard />
</StackPanel>
</Panel>
</Design.PreviewWith>
<Style Selector="controls|PromptExpansionCard">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="12">
<sg:SpacedGrid
ColumnDefinitions="Auto,*"
ColumnSpacing="8"
RowDefinitions="*,*,*,*"
RowSpacing="0">
<!-- Model -->
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Model}"
TextAlignment="Left" />
<ui:FAComboBox
x:Name="PART_ModelComboBox"
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Stretch"
ItemContainerTheme="{StaticResource FAComboBoxItemHybridModelTheme}"
ItemsSource="{Binding ClientManager.PromptExpansionModels}"
SelectedItem="{Binding SelectedModel}">
<ui:FAComboBox.Resources>
<input:StandardUICommand x:Key="RemoteDownloadCommand"
Command="{Binding RemoteDownloadCommand}" />
</ui:FAComboBox.Resources>
<ui:FAComboBox.DataTemplates>
<controls:HybridModelTemplateSelector>
<DataTemplate x:Key="{x:Static models:HybridModelType.Downloadable}" DataType="models:HybridModelFile">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Foreground="{DynamicResource ThemeGreyColor}" Text="{Binding ShortDisplayName}" />
<Button
Grid.Column="1"
Margin="8,0,0,0"
Padding="0"
Classes="transparent-full">
<fluentIcons:SymbolIcon
VerticalAlignment="Center"
FontSize="18"
Foreground="{DynamicResource ThemeGreyColor}"
IsFilled="True"
Symbol="CloudArrowDown" />
</Button>
</Grid>
</DataTemplate>
<DataTemplate x:Key="{x:Static models:HybridModelType.None}" DataType="models:HybridModelFile">
<TextBlock Text="{Binding ShortDisplayName}" />
</DataTemplate>
</controls:HybridModelTemplateSelector>
</ui:FAComboBox.DataTemplates>
</ui:FAComboBox>
<!--<controls:BetterComboBox
Grid.Row="0"
Grid.Column="1"
Padding="8,6,4,6"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Upscalers}"
SelectedItem="{Binding SelectedModel}"
Theme="{StaticResource BetterComboBoxHybridModelTheme}" />-->
</sg:SpacedGrid>
</controls:Card>
</ControlTemplate>
</Setter>
</Style>
</Styles>

52
StabilityMatrix.Avalonia/Controls/Inference/PromptExpansionCard.axaml.cs

@ -0,0 +1,52 @@
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class PromptExpansionCard : TemplatedControl
{
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var upscalerComboBox = e.NameScope.Find("PART_ModelComboBox") as FAComboBox;
upscalerComboBox!.SelectionChanged += UpscalerComboBox_OnSelectionChanged;
}
private void UpscalerComboBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count == 0)
return;
var item = e.AddedItems[0];
if (item is HybridModelFile { IsDownloadable: true })
{
// Reset the selection
e.Handled = true;
if (
e.RemovedItems.Count > 0
&& e.RemovedItems[0] is HybridModelFile { IsDownloadable: false } removedItem
)
{
(sender as FAComboBox)!.SelectedItem = removedItem;
}
else
{
(sender as FAComboBox)!.SelectedItem = null;
}
// Show dialog to download the model
(DataContext as PromptExpansionCardViewModel)!
.RemoteDownloadCommand.ExecuteAsync(item)
.SafeFireAndForget();
}
}
}

8
StabilityMatrix.Avalonia/Controls/Inference/SamplerCard.axaml

@ -123,7 +123,13 @@
<StackPanel>
<!-- Denoise Strength -->
<StackPanel IsVisible="{Binding IsDenoiseStrengthEnabled}">
<StackPanel>
<StackPanel.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.Or}">
<Binding Path="IsDenoiseStrengthEnabled"/>
<Binding Path="IsDenoiseStrengthTempEnabled"/>
</MultiBinding>
</StackPanel.IsVisible>
<Grid ColumnDefinitions="*,Auto">
<TextBlock
VerticalAlignment="Center"

2
StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml

@ -3,7 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="using:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="using:FluentAvalonia.UI.Controls"

6
StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml

@ -4,7 +4,7 @@
xmlns:local="clr-namespace:StabilityMatrix.Avalonia"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:modules="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference.Modules"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
@ -39,7 +39,7 @@
Padding="12,4"
IsEnabled="{Binding !!Cards.Count}"
IsChecked="{Binding IsEditEnabled}">
<fluentIcons:SymbolIcon Symbol="Edit" />
<fluentIcons:SymbolIcon Symbol="Edit" FontSize="14" />
</ToggleButton>
<!-- Add button -->
<Button Grid.Column="3"
@ -47,7 +47,7 @@
VerticalAlignment="Bottom"
CornerRadius="4"
Padding="12,4" >
<fluentIcons:SymbolIcon Symbol="Add" IsFilled="True" />
<fluentIcons:SymbolIcon Symbol="Add" IsFilled="True" FontSize="14" />
</Button>
</sg:SpacedGrid>

6
StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml

@ -2,11 +2,11 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:local="clr-namespace:StabilityMatrix.Avalonia"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:fluent="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
x:DataType="vmInference:StackExpanderViewModel">
<Design.PreviewWith>
@ -66,7 +66,7 @@
Classes="transparent-full"
Command="{Binding SettingsCommand}"
IsVisible="{Binding IsSettingsEnabled}">
<fluentIcons:SymbolIcon FontSize="17" Symbol="Settings" />
<fluent:SymbolIcon FontSize="17" Symbol="Settings" />
</Button>
<!-- Delete button for StackEditableCard -->
<Button
@ -76,7 +76,7 @@
Classes="transparent-red"
Command="{Binding RemoveFromParentListCommand}"
IsVisible="{Binding IsEditEnabled}">
<fluentIcons:SymbolIcon FontSize="16" Symbol="Delete" />
<fluent:SymbolIcon FontSize="16" Symbol="Delete" />
</Button>
</Grid>
</Expander.Header>

2
StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml

@ -8,7 +8,7 @@
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:vmInference="using:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
x:DataType="vmInference:UpscalerCardViewModel">
<Design.PreviewWith>
<Grid MinWidth="300">

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);

4
StabilityMatrix.Avalonia/Controls/StarsRating.axaml

@ -1,8 +1,6 @@
<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:icons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia">
xmlns:controls="using:StabilityMatrix.Avalonia.Controls">
<Design.PreviewWith>
<StackPanel Width="400" Height="400" Spacing="4">
<StackPanel.Styles>

15
StabilityMatrix.Avalonia/Controls/StarsRating.axaml.cs

@ -3,17 +3,12 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Media;
using Avalonia.VisualTree;
using FluentIcons.Avalonia.Fluent;
using FluentIcons.Common;
using FluentIcons.FluentAvalonia;
using SpacedGridControl.Avalonia;
using StabilityMatrix.Avalonia.Styles;
namespace StabilityMatrix.Avalonia.Controls;
@ -36,10 +31,10 @@ public class StarsRating : TemplatedControl
set => SetValue(IsEditableProperty, value);
}
public static readonly StyledProperty<int> MaximumProperty = AvaloniaProperty.Register<
StarsRating,
int
>(nameof(Maximum), 5);
public static readonly StyledProperty<int> MaximumProperty = AvaloniaProperty.Register<StarsRating, int>(
nameof(Maximum),
5
);
public int Maximum
{

65
StabilityMatrix.Avalonia/Controls/TreeFileExplorer.axaml

@ -0,0 +1,65 @@
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:system="using:System"
xmlns:treeFileExplorer="clr-namespace:StabilityMatrix.Avalonia.Models.TreeFileExplorer"
xmlns:mock="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent">
<Design.PreviewWith>
<StackPanel Spacing="16">
<Panel Height="300" Margin="4">
<controls:TreeFileExplorer
RootPath="{x:Static mock:DesignData.CurrentDirectory}" />
</Panel>
<Panel Height="300" Margin="4">
<controls:TreeFileExplorer
IndexFiles="False"
CanSelectFiles="False"
RootPath="{x:Static mock:DesignData.CurrentDirectory}" />
</Panel>
</StackPanel>
</Design.PreviewWith>
<Style Selector="controls|TreeFileExplorer">
<!-- Set Defaults -->
<Setter Property="Template">
<ControlTemplate>
<Grid>
<TreeView
ScrollViewer.VerticalScrollBarVisibility="Auto"
ItemsSource="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=RootItem.Children}">
<TreeView.DataTemplates>
<DataTemplate
DataType="treeFileExplorer:TreeFileExplorerFile">
<sg:SpacedGrid ColumnDefinitions="Auto,*" RowSpacing="0" ColumnSpacing="4">
<fluentIcons:SymbolIcon
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Symbol="Document" />
<TextBlock
Grid.Column="1"
Text="{Binding Path.Name}" />
</sg:SpacedGrid>
</DataTemplate>
<TreeDataTemplate
DataType="treeFileExplorer:TreeFileExplorerDirectory"
ItemsSource="{Binding Children}">
<sg:SpacedGrid ColumnDefinitions="Auto,*" RowSpacing="0" ColumnSpacing="4">
<fluentIcons:SymbolIcon
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
IsFilled="True"
Symbol="Folder" />
<TextBlock
Grid.Column="1"
Text="{Binding Path.Name}" />
</sg:SpacedGrid>
</TreeDataTemplate>
</TreeView.DataTemplates>
</TreeView>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Styles>

135
StabilityMatrix.Avalonia/Controls/TreeFileExplorer.axaml.cs

@ -0,0 +1,135 @@
using Avalonia;
using Avalonia.Controls.Primitives;
using StabilityMatrix.Avalonia.Models.TreeFileExplorer;
using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Avalonia.Controls;
public class TreeFileExplorer : TemplatedControl
{
public static readonly StyledProperty<TreeFileExplorerDirectory?> RootItemProperty =
AvaloniaProperty.Register<TreeFileExplorer, TreeFileExplorerDirectory?>("RootItem");
public TreeFileExplorerDirectory? RootItem
{
get => GetValue(RootItemProperty);
set => SetValue(RootItemProperty, value);
}
public static readonly StyledProperty<string?> RootPathProperty = AvaloniaProperty.Register<
TreeFileExplorer,
string?
>("RootPath");
public string? RootPath
{
get => GetValue(RootPathProperty);
set => SetValue(RootPathProperty, value);
}
public static readonly StyledProperty<IPathObject?> SelectedPathProperty = AvaloniaProperty.Register<
TreeFileExplorer,
IPathObject?
>("SelectedPath");
public IPathObject? SelectedPath
{
get => GetValue(SelectedPathProperty);
set => SetValue(SelectedPathProperty, value);
}
public static readonly StyledProperty<bool> CanSelectFilesProperty = AvaloniaProperty.Register<
TreeFileExplorer,
bool
>("CanSelectFiles", true);
public bool CanSelectFiles
{
get => GetValue(CanSelectFilesProperty);
set => SetValue(CanSelectFilesProperty, value);
}
public static readonly StyledProperty<bool> CanSelectFoldersProperty = AvaloniaProperty.Register<
TreeFileExplorer,
bool
>("CanSelectFolders", true);
public bool CanSelectFolders
{
get => GetValue(CanSelectFoldersProperty);
set => SetValue(CanSelectFoldersProperty, value);
}
public static readonly StyledProperty<bool> IndexFilesProperty = AvaloniaProperty.Register<
TreeFileExplorer,
bool
>("IndexFiles", true);
public bool IndexFiles
{
get => GetValue(IndexFilesProperty);
set => SetValue(IndexFilesProperty, value);
}
public static readonly StyledProperty<bool> IndexFoldersProperty = AvaloniaProperty.Register<
TreeFileExplorer,
bool
>("IndexFolders", true);
public bool IndexFolders
{
get => GetValue(IndexFoldersProperty);
set => SetValue(IndexFoldersProperty, value);
}
private TreeFileExplorerOptions GetOptions()
{
var options = TreeFileExplorerOptions.None;
if (CanSelectFiles)
{
options |= TreeFileExplorerOptions.CanSelectFiles;
}
if (CanSelectFolders)
{
options |= TreeFileExplorerOptions.CanSelectFolders;
}
if (IndexFiles)
{
options |= TreeFileExplorerOptions.IndexFiles;
}
if (IndexFolders)
{
options |= TreeFileExplorerOptions.IndexFolders;
}
return options;
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (RootItem is null)
{
RootItem = RootPath is null
? null
: new TreeFileExplorerDirectory(new DirectoryPath(RootPath), GetOptions());
}
}
/// <inheritdoc />
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == RootPathProperty)
{
var path = change.GetNewValue<string?>();
RootItem = path is null
? null
: new TreeFileExplorerDirectory(new DirectoryPath(path), GetOptions());
}
}
}

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>

122
StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml

@ -0,0 +1,122 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
x:DataType="video:SvdImgToVidConditioningViewModel"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:video="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference.Video"
xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages">
<Design.PreviewWith>
<Grid MinWidth="400">
<controls:VideoGenerationSettingsCard DataContext="{x:Static mocks:DesignData.SvdImgToVidConditioningViewModel}" />
</Grid>
</Design.PreviewWith>
<Style Selector="controls|VideoGenerationSettingsCard">
<!-- Set Defaults -->
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="8" HorizontalAlignment="{TemplateBinding HorizontalAlignment}">
<Grid Margin="4" RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto, *">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="0,0,8,0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Frames}" />
<controls1:NumberBox
Grid.Row="0"
Grid.Column="1"
SelectionHighlightColor="Transparent"
Value="{Binding NumFrames}"
Margin="8,0,0,0"
SimpleNumberFormat="F0"
SmallChange="1"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Inline"/>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Fps}" />
<controls1:NumberBox
Grid.Row="1"
Grid.Column="1"
SelectionHighlightColor="Transparent"
Value="{Binding Fps}"
Margin="8,8,0,0"
SimpleNumberFormat="F0"
SmallChange="1"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Inline"/>
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_MinCfg}" />
<controls1:NumberBox
Margin="8,8,0,0"
Grid.Row="2"
Grid.Column="1"
SelectionHighlightColor="Transparent"
Value="{Binding MinCfg}"
SimpleNumberFormat="F0"
SmallChange="1"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Inline"/>
<TextBlock
Margin="0,8,8,0"
Grid.Row="3"
Grid.Column="0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_MotionBucketId}" />
<controls1:NumberBox
Margin="8,8,0,0"
Grid.Row="3"
Grid.Column="1"
SelectionHighlightColor="Transparent"
Value="{Binding MotionBucketId}"
SimpleNumberFormat="F0"
SmallChange="1"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Inline"/>
<StackPanel Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="4"
Margin="0,16,0,0">
<Grid ColumnDefinitions="*,Auto">
<TextBlock
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_AugmentationLevel}" />
<controls1:NumberBox
Grid.Column="1"
Margin="4,0,0,0"
ValidationMode="InvalidInputOverwritten"
SmallChange="0.01"
SimpleNumberFormat="F2"
Value="{Binding AugmentationLevel}"
HorizontalAlignment="Stretch"
MinWidth="100"
SpinButtonPlacementMode="Compact"/>
</Grid>
<Slider
Minimum="0"
Maximum="10"
Value="{Binding AugmentationLevel}"
TickFrequency="1"
Margin="0,0,0,-4"
TickPlacement="BottomRight"/>
</StackPanel>
</Grid>
</controls:Card>
</ControlTemplate>
</Setter>
</Style>
</Styles>

7
StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml.cs

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

98
StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml

@ -0,0 +1,98 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
x:DataType="video:VideoOutputSettingsCardViewModel"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:video="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference.Video"
xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages">
<Design.PreviewWith>
<Grid MinWidth="400">
<controls:VideoOutputSettingsCard
DataContext="{x:Static mocks:DesignData.SvdImgToVidConditioningViewModel}" />
</Grid>
</Design.PreviewWith>
<Style Selector="controls|VideoOutputSettingsCard">
<!-- Set Defaults -->
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="8" HorizontalAlignment="{TemplateBinding HorizontalAlignment}">
<Grid Margin="4" RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto, *">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Text="Video Output Settings"
FontSize="16"
FontWeight="DemiBold"
Margin="0,0,0,16"
/>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="0,0,8,0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Fps}" />
<controls1:NumberBox
Grid.Row="1"
Grid.Column="1"
SelectionHighlightColor="Transparent"
Value="{Binding Fps}"
Margin="8,0,0,0"
SimpleNumberFormat="F0"
SmallChange="1"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Inline" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Lossless}" />
<CheckBox
Grid.Row="2"
Grid.Column="1"
IsChecked="{Binding Lossless}"
Margin="8,8,0,0"
HorizontalAlignment="Stretch" />
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_VideoQuality}" />
<controls1:NumberBox
Margin="8,8,0,0"
Grid.Row="3"
Grid.Column="1"
SelectionHighlightColor="Transparent"
Value="{Binding Quality}"
SimpleNumberFormat="F0"
SmallChange="1"
Maximum="100"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Inline" />
<TextBlock
Margin="0,8,8,0"
Grid.Row="4"
Grid.Column="0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_VideoOutputMethod}" />
<ComboBox
Grid.Row="4"
Grid.Column="1"
Margin="8,8,0,0"
MinWidth="100"
ItemsSource="{Binding AvailableMethods}"
SelectedIndex="{Binding SelectedMethod}" />
</Grid>
</controls:Card>
</ControlTemplate>
</Setter>
</Style>
</Styles>

7
StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml.cs

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

40
StabilityMatrix.Avalonia/Converters/EnumAttributeConverter.cs

@ -0,0 +1,40 @@
using System;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Avalonia.Data.Converters;
namespace StabilityMatrix.Avalonia.Converters;
/// <summary>
/// Converts an enum value to an attribute
/// </summary>
/// <typeparam name="TAttribute">Type of attribute</typeparam>
public class EnumAttributeConverter<TAttribute>(Func<TAttribute, object?>? accessor = null) : IValueConverter
where TAttribute : Attribute
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is null)
return null;
if (value is not Enum @enum)
throw new ArgumentException("Value must be an enum");
var field = @enum.GetType().GetField(@enum.ToString());
if (field is null)
throw new ArgumentException("Value must be an enum");
if (field.GetCustomAttributes<TAttribute>().FirstOrDefault() is not { } attribute)
throw new ArgumentException($"Enum value {@enum} does not have attribute {typeof(TAttribute)}");
return accessor is not null ? accessor(attribute) : attribute;
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}

13
StabilityMatrix.Avalonia/Converters/EnumAttributeConverters.cs

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace StabilityMatrix.Avalonia.Converters;
internal static class EnumAttributeConverters
{
public static EnumAttributeConverter<DisplayAttribute> Display => new();
public static EnumAttributeConverter<DisplayAttribute> DisplayName => new(attribute => attribute.Name);
public static EnumAttributeConverter<DisplayAttribute> DisplayDescription =>
new(attribute => attribute.Description);
}

37
StabilityMatrix.Avalonia/Converters/FileUriConverter.cs

@ -0,0 +1,37 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using StabilityMatrix.Core.Extensions;
namespace StabilityMatrix.Avalonia.Converters;
public class FileUriConverter : IValueConverter
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (targetType != typeof(Uri))
{
return null;
}
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
};
}
/// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (targetType == typeof(string) && value is Uri uri)
{
return uri.ToString().StripStart("file://");
}
return null;
}
}

53
StabilityMatrix.Avalonia/Converters/FuncCommandConverter.cs

@ -0,0 +1,53 @@
using System;
using System.Globalization;
using System.Windows.Input;
using Avalonia.Data.Converters;
using PropertyModels.ComponentModel;
namespace StabilityMatrix.Avalonia.Converters;
/// <summary>
/// Converts an object's named <see cref="Func{TResult}"/> to a <see cref="ICommand"/>.
/// </summary>
public class FuncCommandConverter : IValueConverter
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is null || parameter is null)
{
return null;
}
// Parameter is the name of the Func<T> to convert.
if (parameter is not string funcName)
{
// ReSharper disable once LocalizableElement
throw new ArgumentException("Parameter must be a string.", nameof(parameter));
}
// Find the Func<T> on the object.
if (value.GetType().GetMethod(funcName) is not { } methodInfo)
{
// ReSharper disable once LocalizableElement
throw new ArgumentException(
$"Method {funcName} not found on {value.GetType().Name}.",
nameof(parameter)
);
}
// Create a delegate from the method info.
var func = (Action)methodInfo.CreateDelegate(typeof(Action), value);
// Create ICommand
var command = ReactiveCommand.Create(func);
return command;
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}

344
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -4,15 +4,14 @@ using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using AvaloniaEdit.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using DynamicData.Binding;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Semver;
using StabilityMatrix.Avalonia.Controls.CodeCompletion;
using StabilityMatrix.Avalonia.Models;
@ -24,8 +23,9 @@ using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
using StabilityMatrix.Avalonia.ViewModels.Inference.Video;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.ViewModels.Settings;
using StabilityMatrix.Core.Api;
@ -36,9 +36,12 @@ 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;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Packages.Extensions;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Python;
@ -49,6 +52,7 @@ using HuggingFacePageViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointB
namespace StabilityMatrix.Avalonia.DesignData;
[Localizable(false)]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public static class DesignData
{
@ -96,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
@ -175,12 +192,24 @@ public static class DesignData
};
LaunchOptionsViewModel.UpdateFilterCards();
InstallerViewModel = Services.GetRequiredService<InstallerViewModel>();
InstallerViewModel.AvailablePackages = new ObservableCollectionExtended<BasePackage>(
packageFactory.GetAllAvailablePackages()
NewInstallerDialogViewModel = Services.GetRequiredService<PackageInstallBrowserViewModel>();
// NewInstallerDialogViewModel.InferencePackages = new ObservableCollectionExtended<BasePackage>(
// packageFactory.GetPackagesByType(PackageType.SdInference).OrderBy(p => p.InstallerSortOrder)
// );
// NewInstallerDialogViewModel.TrainingPackages = new ObservableCollection<BasePackage>(
// packageFactory.GetPackagesByType(PackageType.SdTraining).OrderBy(p => p.InstallerSortOrder)
// );
PackageInstallDetailViewModel = new PackageInstallDetailViewModel(
packageFactory.GetAllAvailablePackages().FirstOrDefault() as BaseGitPackage,
settingsManager,
notificationService,
null,
null,
null,
null,
packageFactory
);
InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0];
InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note.";
ObservableCacheEx.AddOrUpdate(
CheckpointsPageViewModel.CheckpointFoldersCache,
@ -198,6 +227,7 @@ public static class DesignData
PreviewImagePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/"
+ "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
UpdateAvailable = true,
ConnectedModel = new ConnectedModelInfo
{
VersionName = "Lightning Auroral",
@ -297,7 +327,7 @@ public static class DesignData
);
}*/
CivitAiBrowserViewModel.ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>
CivitAiBrowserViewModel.ModelCards = new ObservableCollectionExtended<CheckpointBrowserCardViewModel>
{
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
@ -330,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"
@ -373,13 +403,15 @@ public static class DesignData
new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" }
};
ProgressManagerViewModel
.ProgressItems
.AddRange(
ProgressManagerViewModel.ProgressItems.AddRange(
new ProgressItemViewModelBase[]
{
new ProgressItemViewModel(
new ProgressItem(Guid.NewGuid(), "Test File.exe", new ProgressReport(0.5f, "Downloading..."))
new ProgressItem(
Guid.NewGuid(),
"Test File.exe",
new ProgressReport(0.5f, "Downloading...")
)
),
new MockDownloadProgressItemViewModel(
"Very Long Test File Name Need Even More Longness Thanks That's pRobably good 2.exe"
@ -387,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"
}
)
}
@ -402,7 +435,10 @@ public static class DesignData
}
[NotNull]
public static InstallerViewModel? InstallerViewModel { get; private set; }
public static PackageInstallBrowserViewModel? NewInstallerDialogViewModel { get; private set; }
[NotNull]
public static PackageInstallDetailViewModel? PackageInstallDetailViewModel { get; private set; }
[NotNull]
public static LaunchOptionsViewModel? LaunchOptionsViewModel { get; private set; }
@ -413,16 +449,64 @@ public static class DesignData
public static ServiceManager<ViewModelBase> DialogFactory =>
Services.GetRequiredService<ServiceManager<ViewModelBase>>();
public static MainWindowViewModel MainWindowViewModel => Services.GetRequiredService<MainWindowViewModel>();
public static MainWindowViewModel MainWindowViewModel =>
Services.GetRequiredService<MainWindowViewModel>();
public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel =>
Services.GetRequiredService<FirstLaunchSetupViewModel>();
public static LaunchPageViewModel LaunchPageViewModel => Services.GetRequiredService<LaunchPageViewModel>();
public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<LaunchPageViewModel>();
public static HuggingFacePageViewModel HuggingFacePageViewModel =>
Services.GetRequiredService<HuggingFacePageViewModel>();
public static NewOneClickInstallViewModel NewOneClickInstallViewModel =>
Services.GetRequiredService<NewOneClickInstallViewModel>();
public static RecommendedModelsViewModel RecommendedModelsViewModel =>
DialogFactory.Get<RecommendedModelsViewModel>(vm =>
{
vm.Sd15Models = new ObservableCollectionExtended<RecommendedModelItemViewModel>()
{
new()
{
ModelVersion = new CivitModelVersion
{
Name = "BB95 Furry Mix",
Description = "A furry mix of BB95",
Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 },
Images =
[
new CivitImage
{
Url =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg"
}
],
},
Author = "bb95"
},
new()
{
ModelVersion = new CivitModelVersion
{
Name = "BB95 Furry Mix",
Description = "A furry mix of BB95",
Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 },
Images =
[
new CivitImage
{
Url =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg"
}
],
},
Author = "bb95"
}
};
});
public static OutputsPageViewModel OutputsPageViewModel
{
get
@ -439,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;
}
}
@ -452,7 +546,10 @@ public static class DesignData
vm.SetPackages(settings.Settings.InstalledPackages);
vm.SetUnknownPackages(
new InstalledPackage[] { UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"), }
new InstalledPackage[]
{
UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"),
}
);
vm.PackageCards[0].IsUpdateAvailable = true;
@ -461,6 +558,39 @@ public static class DesignData
}
}
public static PackageExtensionBrowserViewModel PackageExtensionBrowserViewModel =>
DialogFactory.Get<PackageExtensionBrowserViewModel>(vm =>
{
vm.AddExtensions(
[
new PackageExtension
{
Author = "123",
Title = "Cool Extension",
Description = "This is an interesting extension",
Reference = new Uri("https://github.com/LykosAI/StabilityMatrix"),
Files = [new Uri("https://github.com/LykosAI/StabilityMatrix")]
},
new PackageExtension
{
Author = "123",
Title = "Cool Extension",
Description = "This is an interesting extension",
Reference = new Uri("https://github.com/LykosAI/StabilityMatrix"),
Files = [new Uri("https://github.com/LykosAI/StabilityMatrix")]
}
],
[
new InstalledPackageExtension
{
GitRepositoryUrl = "https://github.com/LykosAI/StabilityMatrix",
Paths = [new DirectoryPath("example-dir")]
},
new InstalledPackageExtension { Paths = [new DirectoryPath("example-dir-2")] }
]
);
});
public static CheckpointsPageViewModel CheckpointsPageViewModel =>
Services.GetRequiredService<CheckpointsPageViewModel>();
@ -469,14 +599,21 @@ public static class DesignData
public static SettingsViewModel SettingsViewModel => Services.GetRequiredService<SettingsViewModel>();
public static NewPackageManagerViewModel NewPackageManagerViewModel =>
Services.GetRequiredService<NewPackageManagerViewModel>();
public static InferenceSettingsViewModel InferenceSettingsViewModel =>
Services.GetRequiredService<InferenceSettingsViewModel>();
public static MainSettingsViewModel MainSettingsViewModel => Services.GetRequiredService<MainSettingsViewModel>();
public static MainSettingsViewModel MainSettingsViewModel =>
Services.GetRequiredService<MainSettingsViewModel>();
public static AccountSettingsViewModel AccountSettingsViewModel =>
Services.GetRequiredService<AccountSettingsViewModel>();
public static NotificationSettingsViewModel NotificationSettingsViewModel =>
Services.GetRequiredService<NotificationSettingsViewModel>();
public static UpdateSettingsViewModel UpdateSettingsViewModel
{
get
@ -556,7 +693,10 @@ The gallery images are often inpainted, but you will get something very similar
}
}
};
var sampleViewModel = new ModelVersionViewModel(new HashSet<string> { "ABCD" }, sampleCivitVersions[0]);
var sampleViewModel = new ModelVersionViewModel(
new HashSet<string> { "ABCD" },
sampleCivitVersions[0]
);
// Sample data for dialogs
vm.Versions = new List<ModelVersionViewModel> { sampleViewModel };
@ -630,13 +770,22 @@ The gallery images are often inpainted, but you will get something very similar
vm.OutputProgress.Text = "Sampler 10/30";
});
public static InferenceImageToVideoViewModel InferenceImageToVideoViewModel =>
DialogFactory.Get<InferenceImageToVideoViewModel>(vm =>
{
vm.OutputProgress.Value = 10;
vm.OutputProgress.Maximum = 30;
vm.OutputProgress.Text = "Sampler 10/30";
});
public static InferenceImageToImageViewModel InferenceImageToImageViewModel =>
DialogFactory.Get<InferenceImageToImageViewModel>();
public static InferenceImageUpscaleViewModel InferenceImageUpscaleViewModel =>
DialogFactory.Get<InferenceImageUpscaleViewModel>();
public static PackageImportViewModel PackageImportViewModel => DialogFactory.Get<PackageImportViewModel>();
public static PackageImportViewModel PackageImportViewModel =>
DialogFactory.Get<PackageImportViewModel>();
public static RefreshBadgeViewModel RefreshBadgeViewModel => new() { State = ProgressState.Success };
@ -652,6 +801,8 @@ The gallery images are often inpainted, but you will get something very similar
});
public static SeedCardViewModel SeedCardViewModel => new();
public static SvdImgToVidConditioningViewModel SvdImgToVidConditioningViewModel => new();
public static VideoOutputSettingsCardViewModel VideoOutputSettingsCardViewModel => new();
public static SamplerCardViewModel SamplerCardViewModel =>
DialogFactory.Get<SamplerCardViewModel>(vm =>
@ -681,6 +832,9 @@ The gallery images are often inpainted, but you will get something very similar
public static ModelCardViewModel ModelCardViewModel => DialogFactory.Get<ModelCardViewModel>();
public static ImgToVidModelCardViewModel ImgToVidModelCardViewModel =>
DialogFactory.Get<ImgToVidModelCardViewModel>();
public static ImageGalleryCardViewModel ImageGalleryCardViewModel =>
DialogFactory.Get<ImageGalleryCardViewModel>(vm =>
{
@ -706,7 +860,8 @@ The gallery images are often inpainted, but you will get something very similar
);
});
public static ImageFolderCardViewModel ImageFolderCardViewModel => DialogFactory.Get<ImageFolderCardViewModel>();
public static ImageFolderCardViewModel ImageFolderCardViewModel =>
DialogFactory.Get<ImageFolderCardViewModel>();
public static FreeUCardViewModel FreeUCardViewModel => DialogFactory.Get<FreeUCardViewModel>();
@ -759,7 +914,8 @@ The gallery images are often inpainted, but you will get something very similar
public static UpscalerCardViewModel UpscalerCardViewModel => DialogFactory.Get<UpscalerCardViewModel>();
public static BatchSizeCardViewModel BatchSizeCardViewModel => DialogFactory.Get<BatchSizeCardViewModel>();
public static BatchSizeCardViewModel BatchSizeCardViewModel =>
DialogFactory.Get<BatchSizeCardViewModel>();
public static BatchSizeCardViewModel BatchSizeCardViewModelWithIndexOption =>
DialogFactory.Get<BatchSizeCardViewModel>(vm =>
@ -767,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>
{
@ -790,6 +979,42 @@ The gallery images are often inpainted, but you will get something very similar
}
}
public static IEnumerable<HybridModelFile> SampleHybridModels { get; } =
[
HybridModelFile.FromLocal(
new LocalModelFile
{
SharedFolderType = SharedFolderType.StableDiffusion,
RelativePath = "art_shaper_v8.safetensors",
PreviewImageFullPath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dd9b038c-bd15-43ab-86ab-66e145ad7ff2/width=512",
ConnectedModelInfo = new ConnectedModelInfo
{
ModelName = "Art Shaper (very long name example)",
VersionName = "Style v8 (very long name)"
}
}
),
HybridModelFile.FromLocal(
new LocalModelFile
{
SharedFolderType = SharedFolderType.StableDiffusion,
RelativePath = "background_arts.safetensors",
PreviewImageFullPath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/71c81ddf-d8c3-46b4-843d-9f8f20a9254a/width=512",
ConnectedModelInfo = new ConnectedModelInfo
{
ModelName = "Background Arts",
VersionName = "Anime Style v10"
}
}
),
HybridModelFile.FromRemote("v1-5-pruned-emaonly.safetensors"),
HybridModelFile.FromRemote("sample-model.pt"),
];
public static HybridModelFile SampleHybridModel => SampleHybridModels.First();
public static ImageViewerViewModel ImageViewerViewModel =>
DialogFactory.Get<ImageViewerViewModel>(vm =>
{
@ -816,7 +1041,8 @@ The gallery images are often inpainted, but you will get something very similar
public static InferenceConnectionHelpViewModel InferenceConnectionHelpViewModel =>
DialogFactory.Get<InferenceConnectionHelpViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel => DialogFactory.Get<SelectImageCardViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel =>
DialogFactory.Get<SelectImageCardViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel_WithImage =>
DialogFactory.Get<SelectImageCardViewModel>(vm =>
@ -829,12 +1055,73 @@ The gallery images are often inpainted, but you will get something very similar
});
public static ImageSource SampleImageSource =>
new(new Uri("https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500"))
new(
new Uri(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500"
)
)
{
Label = "Test Image"
};
public static ControlNetCardViewModel ControlNetCardViewModel => DialogFactory.Get<ControlNetCardViewModel>();
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();
@ -844,7 +1131,8 @@ The gallery images are often inpainted, but you will get something very similar
{
get
{
var type = Type.GetType(typeName) ?? throw new ArgumentException($"Type {typeName} not found");
var type =
Type.GetType(typeName) ?? throw new ArgumentException($"Type {typeName} not found");
try
{
return Services.GetService(type);

21
StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs

@ -27,6 +27,9 @@ public partial class MockInferenceClientManager : ObservableObject, IInferenceCl
public IObservableCollection<HybridModelFile> ControlNetModels { get; } =
new ObservableCollectionExtended<HybridModelFile>();
public IObservableCollection<HybridModelFile> PromptExpansionModels { get; } =
new ObservableCollectionExtended<HybridModelFile>();
public IObservableCollection<ComfySampler> Samplers { get; } =
new ObservableCollectionExtended<ComfySampler>(ComfySampler.Defaults);
@ -38,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;
@ -64,28 +70,19 @@ public partial class MockInferenceClientManager : ObservableObject, IInferenceCl
}
/// <inheritdoc />
public Task CopyImageToInputAsync(
FilePath imageFile,
CancellationToken cancellationToken = default
)
public Task CopyImageToInputAsync(FilePath imageFile, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
/// <inheritdoc />
public Task UploadInputImageAsync(
ImageSource image,
CancellationToken cancellationToken = default
)
public Task UploadInputImageAsync(ImageSource image, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
/// <inheritdoc />
public Task WriteImageToInputAsync(
ImageSource imageSource,
CancellationToken cancellationToken = default
)
public Task WriteImageToInputAsync(ImageSource imageSource, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}

22
StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
@ -25,9 +26,26 @@ public class MockModelIndexService : IModelIndexService
}
/// <inheritdoc />
public Task<IReadOnlyList<LocalModelFile>> GetModelsOfType(SharedFolderType type)
public Task<IEnumerable<LocalModelFile>> FindAsync(SharedFolderType type)
{
return Task.FromResult<IReadOnlyList<LocalModelFile>>(new List<LocalModelFile>());
return Task.FromResult(Enumerable.Empty<LocalModelFile>());
}
/// <inheritdoc />
public Task<IEnumerable<LocalModelFile>> FindByHashAsync(string hashBlake3)
{
return Task.FromResult(Enumerable.Empty<LocalModelFile>());
}
/// <inheritdoc />
public Task<bool> RemoveModelAsync(LocalModelFile model)
{
return Task.FromResult(false);
}
public Task CheckModelsForUpdates()
{
return Task.CompletedTask;
}
/// <inheritdoc />

19
StabilityMatrix.Avalonia/DesignData/MockSettingsManager.cs

@ -1,9 +1,22 @@
using StabilityMatrix.Core.Services;
using System.Threading;
using System.Threading.Tasks;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.DesignData;
public class MockSettingsManager : SettingsManager
{
protected override void LoadSettings() {}
protected override void SaveSettings() {}
protected override void LoadSettings(CancellationToken cancellationToken = default) { }
protected override Task LoadSettingsAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
protected override void SaveSettings(CancellationToken cancellationToken = default) { }
protected override Task SaveSettingsAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}

49
StabilityMatrix.Avalonia/DialogHelper.cs

@ -23,15 +23,15 @@ using NLog;
using Refit;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Services;
using TextMateSharp.Grammars;
using Process = FuzzySharp.Process;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Helper;
namespace StabilityMatrix.Avalonia;
@ -48,11 +48,7 @@ public static class DialogHelper
IReadOnlyList<TextBoxField> textFields
)
{
return CreateTextEntryDialog(
title,
new MarkdownScrollViewer { Markdown = description },
textFields
);
return CreateTextEntryDialog(title, new MarkdownScrollViewer { Markdown = description }, textFields);
}
/// <summary>
@ -80,11 +76,7 @@ public static class DialogHelper
var grid = new Grid
{
RowDefinitions =
{
new RowDefinition(GridLength.Star),
new RowDefinition(GridLength.Auto)
},
RowDefinitions = { new RowDefinition(GridLength.Star), new RowDefinition(GridLength.Auto) },
Children = { markdown, image }
};
@ -109,18 +101,14 @@ public static class DialogHelper
var grid = new Grid
{
RowDefinitions =
{
new RowDefinition(GridLength.Auto),
new RowDefinition(GridLength.Star)
},
RowDefinitions = { new RowDefinition(GridLength.Auto), new RowDefinition(GridLength.Star) },
Children = { content, stackPanel }
};
grid.Loaded += (_, _) =>
{
// Focus first TextBox
var firstTextBox = stackPanel.Children
.OfType<StackPanel>()
var firstTextBox = stackPanel
.Children.OfType<StackPanel>()
.FirstOrDefault()
.FindDescendantOfType<TextBox>();
firstTextBox!.Focus();
@ -254,16 +242,16 @@ public static class DialogHelper
Content = viewer,
CloseButtonText = Resources.Action_Close,
IsPrimaryButtonEnabled = false,
MinDialogWidth = 800,
MaxDialogHeight = 1000,
MaxDialogWidth = 1000
};
}
/// <summary>
/// Create a dialog for displaying an ApiException
/// </summary>
public static BetterContentDialog CreateApiExceptionDialog(
ApiException exception,
string? title = null
)
public static BetterContentDialog CreateApiExceptionDialog(ApiException exception, string? title = null)
{
Dispatcher.UIThread.VerifyAccess();
@ -275,9 +263,7 @@ public static class DialogHelper
Options = { ShowColumnRulers = false, AllowScrollBelowDocument = false }
};
var registryOptions = new RegistryOptions(ThemeName.DarkPlus);
textEditor
.InstallTextMate(registryOptions)
.SetGrammar(registryOptions.GetScopeByLanguageId("json"));
textEditor.InstallTextMate(registryOptions).SetGrammar(registryOptions.GetScopeByLanguageId("json"));
var mainGrid = new StackPanel
{
@ -354,9 +340,7 @@ public static class DialogHelper
Options = { ShowColumnRulers = false, AllowScrollBelowDocument = false }
};
var registryOptions = new RegistryOptions(ThemeName.DarkPlus);
textEditor
.InstallTextMate(registryOptions)
.SetGrammar(registryOptions.GetScopeByLanguageId("json"));
textEditor.InstallTextMate(registryOptions).SetGrammar(registryOptions.GetScopeByLanguageId("json"));
var mainGrid = new StackPanel
{
@ -437,8 +421,7 @@ public static class DialogHelper
{
Dispatcher.UIThread.VerifyAccess();
var title =
exception is PromptSyntaxError ? "Prompt Syntax Error" : "Prompt Validation Error";
var title = exception is PromptSyntaxError ? "Prompt Syntax Error" : "Prompt Validation Error";
// Get the index of the error
var errorIndex = exception.TextOffset;
@ -526,7 +509,7 @@ public static class DialogHelper
models.Select(m => m.FileNameWithoutExtension)
);
if (result.Score > 40)
if (result is { Score: > 40 })
{
mainGrid.Children.Add(
new InfoBar

38
StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs

@ -36,15 +36,16 @@ public static class ComfyNodeBuilderExtensions
// If batch index is selected, add a LatentFromBatch
if (batchIndex is not null)
{
builder.Connections.Primary = builder.Nodes
.AddNamedNode(
ComfyNodeBuilder.LatentFromBatch(
"LatentFromBatch",
builder.GetPrimaryAsLatent(),
builder.Connections.Primary = builder
.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;
}
@ -74,15 +75,16 @@ public static class ComfyNodeBuilderExtensions
// If batch index is selected, add a LatentFromBatch
if (batchIndex is not null)
{
builder.Connections.Primary = builder.Nodes
.AddNamedNode(
ComfyNodeBuilder.LatentFromBatch(
"LatentFromBatch",
builder.GetPrimaryAsLatent(),
builder.Connections.Primary = builder
.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,8 +99,8 @@ public static class ComfyNodeBuilderExtensions
_ =>
builder.GetPrimaryAsImage(
builder.Connections.PrimaryVAE
?? builder.Connections.RefinerVAE
?? builder.Connections.BaseVAE
?? builder.Connections.Refiner.VAE
?? builder.Connections.Base.VAE
?? throw new ArgumentException("No Primary, Refiner, or Base VAE")
),
image => image

10
StabilityMatrix.Avalonia/Extensions/DataObjectExtensions.cs

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

20
StabilityMatrix.Avalonia/Extensions/NotificationLevelExtensions.cs

@ -0,0 +1,20 @@
using System;
using Avalonia.Controls.Notifications;
using StabilityMatrix.Core.Models.Settings;
namespace StabilityMatrix.Avalonia.Extensions;
public static class NotificationLevelExtensions
{
public static NotificationType ToNotificationType(this NotificationLevel level)
{
return level switch
{
NotificationLevel.Information => NotificationType.Information,
NotificationLevel.Success => NotificationType.Success,
NotificationLevel.Warning => NotificationType.Warning,
NotificationLevel.Error => NotificationType.Error,
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
};
}
}

54
StabilityMatrix.Avalonia/Extensions/NotificationServiceExtensions.cs

@ -0,0 +1,54 @@
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using DesktopNotifications;
using NLog;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Settings;
namespace StabilityMatrix.Avalonia.Extensions;
public static class NotificationServiceExtensions
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
public static void OnPackageInstallCompleted(
this INotificationService notificationService,
IPackageModificationRunner runner
)
{
OnPackageInstallCompletedAsync(notificationService, runner)
.SafeFireAndForget(ex => Logger.Error(ex, "Error Showing Notification"));
}
private static async Task OnPackageInstallCompletedAsync(
this INotificationService notificationService,
IPackageModificationRunner runner
)
{
if (runner.Failed)
{
Logger.Error(runner.Exception, "Error Installing Package");
await notificationService.ShowAsync(
NotificationKey.Package_Install_Failed,
new Notification
{
Title = runner.ModificationFailedTitle,
Body = runner.ModificationFailedMessage
}
);
}
else
{
await notificationService.ShowAsync(
NotificationKey.Package_Install_Completed,
new Notification
{
Title = runner.ModificationCompleteTitle,
Body = runner.ModificationCompleteMessage
}
);
}
}
}

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();
}
}

24
StabilityMatrix.Avalonia/Helpers/IOCommands.cs

@ -16,4 +16,28 @@ public static class IOCommands
},
url => !string.IsNullOrWhiteSpace(url)
);
public static AsyncRelayCommand<string?> OpenFileBrowserCommand { get; } =
new(
async path =>
{
if (string.IsNullOrWhiteSpace(path))
return;
await ProcessRunner.OpenFileBrowser(path);
},
path => !string.IsNullOrWhiteSpace(path)
);
public static AsyncRelayCommand<string?> OpenFolderBrowserCommand { get; } =
new(
async path =>
{
if (string.IsNullOrWhiteSpace(path))
return;
await ProcessRunner.OpenFolderBrowser(path);
},
path => !string.IsNullOrWhiteSpace(path)
);
}

294
StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs

@ -1,15 +1,21 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia.Controls;
using DynamicData;
using FluentAvalonia.UI.Controls;
using NLog;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python;
@ -19,37 +25,41 @@ namespace StabilityMatrix.Avalonia.Helpers;
[SupportedOSPlatform("macos")]
[SupportedOSPlatform("linux")]
public class UnixPrerequisiteHelper : IPrerequisiteHelper
public class UnixPrerequisiteHelper(
IDownloadService downloadService,
ISettingsManager settingsManager,
IPyRunner pyRunner
) : IPrerequisiteHelper
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager;
private readonly IPyRunner pyRunner;
private DirectoryPath HomeDir => settingsManager.LibraryDir;
private DirectoryPath AssetsDir => HomeDir.JoinDir("Assets");
private DirectoryPath PythonDir => AssetsDir.JoinDir("Python310");
public bool IsPythonInstalled => PythonDir.JoinFile(PyRunner.RelativePythonDllPath).Exists;
private DirectoryPath PortableGitInstallDir => HomeDir + "PortableGit";
public string GitBinPath => PortableGitInstallDir + "bin";
private DirectoryPath NodeDir => AssetsDir.JoinDir("nodejs");
private string NpmPath => Path.Combine(NodeDir, "bin", "npm");
private bool IsNodeInstalled => File.Exists(NpmPath);
private DirectoryPath DotnetDir => AssetsDir.JoinDir("dotnet");
private string DotnetPath => Path.Combine(DotnetDir, "dotnet");
private bool IsDotnetInstalled => File.Exists(DotnetPath);
private string Dotnet7DownloadUrlMacOs =>
"https://download.visualstudio.microsoft.com/download/pr/5bb0e0e4-2a8d-4aba-88ad-232e1f65c281/ee6d35f762d81965b4cf336edde1b318/dotnet-sdk-7.0.405-osx-arm64.tar.gz";
private string Dotnet8DownloadUrlMacOs =>
"https://download.visualstudio.microsoft.com/download/pr/ef083c06-7aee-4a4f-b18b-50c9a8990753/e206864e7910e81bbd9cb7e674ff1b4c/dotnet-sdk-8.0.101-osx-arm64.tar.gz";
private string Dotnet7DownloadUrlLinux =>
"https://download.visualstudio.microsoft.com/download/pr/5202b091-2406-445c-b40a-68a5b97c882b/b509f2a7a0eb61aea145b990b40b6d5b/dotnet-sdk-7.0.405-linux-x64.tar.gz";
private string Dotnet8DownloadUrlLinux =>
"https://download.visualstudio.microsoft.com/download/pr/9454f7dc-b98e-4a64-a96d-4eb08c7b6e66/da76f9c6bc4276332b587b771243ae34/dotnet-sdk-8.0.101-linux-x64.tar.gz";
// Cached store of whether or not git is installed
private bool? isGitInstalled;
public UnixPrerequisiteHelper(
IDownloadService downloadService,
ISettingsManager settingsManager,
IPyRunner pyRunner
)
{
this.downloadService = downloadService;
this.settingsManager = settingsManager;
this.pyRunner = pyRunner;
}
private async Task<bool> CheckIsGitInstalled()
{
var result = await ProcessRunner.RunBashCommand("git --version");
@ -57,34 +67,97 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
return isGitInstalled == true;
}
public async Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null)
public Task InstallPackageRequirements(BasePackage package, IProgress<ProgressReport>? progress = null) =>
InstallPackageRequirements(package.Prerequisites.ToList(), progress);
public async Task InstallPackageRequirements(
List<PackagePrerequisite> prerequisites,
IProgress<ProgressReport>? progress = null
)
{
await UnpackResourcesIfNecessary(progress);
if (prerequisites.Contains(PackagePrerequisite.Python310))
{
await InstallPythonIfNecessary(progress);
await InstallVirtualenvIfNecessary(progress);
}
public async Task UnpackResourcesIfNecessary(IProgress<ProgressReport>? progress = null)
if (prerequisites.Contains(PackagePrerequisite.Git))
{
// Array of (asset_uri, extract_to)
var assets = new[]
await InstallGitIfNecessary(progress);
}
if (prerequisites.Contains(PackagePrerequisite.Node))
{
(Assets.SevenZipExecutable, AssetsDir),
(Assets.SevenZipLicense, AssetsDir),
};
await InstallNodeIfNecessary(progress);
}
if (prerequisites.Contains(PackagePrerequisite.Dotnet))
{
await InstallDotnetIfNecessary(progress);
}
}
public async Task InstallDotnetIfNecessary(IProgress<ProgressReport>? progress = null)
{
if (IsDotnetInstalled)
return;
if (Compat.IsMacOS)
{
await DownloadAndExtractPrerequisite(progress, Dotnet7DownloadUrlMacOs, DotnetDir);
await DownloadAndExtractPrerequisite(progress, Dotnet8DownloadUrlMacOs, DotnetDir);
}
else
{
await DownloadAndExtractPrerequisite(progress, Dotnet7DownloadUrlLinux, DotnetDir);
await DownloadAndExtractPrerequisite(progress, Dotnet8DownloadUrlLinux, DotnetDir);
}
}
private async Task InstallVirtualenvIfNecessary(IProgress<ProgressReport>? progress = null)
{
// python stuff
if (!PyRunner.PipInstalled || !PyRunner.VenvInstalled)
{
progress?.Report(
new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true)
new ProgressReport(-1f, "Installing Python prerequisites...", isIndeterminate: true)
);
await pyRunner.Initialize().ConfigureAwait(false);
if (!PyRunner.PipInstalled)
{
await pyRunner.SetupPip().ConfigureAwait(false);
}
if (!PyRunner.VenvInstalled)
{
await pyRunner.InstallPackage("virtualenv").ConfigureAwait(false);
}
}
}
public async Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null)
{
await UnpackResourcesIfNecessary(progress);
await InstallPythonIfNecessary(progress);
}
public async Task UnpackResourcesIfNecessary(IProgress<ProgressReport>? progress = null)
{
// Array of (asset_uri, extract_to)
var assets = new[] { (Assets.SevenZipExecutable, AssetsDir), (Assets.SevenZipLicense, AssetsDir), };
progress?.Report(new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true));
Directory.CreateDirectory(AssetsDir);
foreach (var (asset, extractDir) in assets)
{
await asset.ExtractToDir(extractDir);
}
progress?.Report(
new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false)
);
progress?.Report(new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false));
}
public async Task InstallGitIfNecessary(IProgress<ProgressReport>? progress = null)
@ -146,8 +219,7 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
if (result.ExitCode != 0)
{
Logger.Error(
"Git command [{Command}] failed with exit code "
+ "{ExitCode}:\n{StdOut}\n{StdErr}",
"Git command [{Command}] failed with exit code " + "{ExitCode}:\n{StdOut}\n{StdErr}",
command,
result.ExitCode,
result.StandardOutput,
@ -235,9 +307,167 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
progress?.Report(new ProgressReport(1, "Installing Python", isIndeterminate: false));
}
public Task<string> GetGitOutput(string? workingDirectory = null, params string[] args)
public Task<ProcessResult> GetGitOutput(ProcessArgs args, string? workingDirectory = null)
{
return ProcessRunner.RunBashCommand(args.Prepend("git").ToArray(), workingDirectory ?? "");
}
[Localizable(false)]
[SupportedOSPlatform("Linux")]
[SupportedOSPlatform("macOS")]
public async Task RunNpm(
ProcessArgs args,
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null,
IReadOnlyDictionary<string, string>? envVars = null
)
{
var process = ProcessRunner.StartAnsiProcess(
NpmPath,
args,
workingDirectory,
onProcessOutput,
envVars
);
await process.WaitForExitAsync();
if (process.ExitCode == 0)
return;
var stdOut = await process.StandardOutput.ReadToEndAsync();
var stdErr = await process.StandardError.ReadToEndAsync();
Logger.Error(
"RunNpm with args [{Args}] failed with exit code " + "{ExitCode}:\n{StdOut}\n{StdErr}",
args,
process.ExitCode,
stdOut,
stdErr
);
throw new ProcessException(
$"RunNpm with args [{args}] failed with exit code" + $" {process.ExitCode}:\n{stdOut}\n{stdErr}"
);
}
[SupportedOSPlatform("Linux")]
[SupportedOSPlatform("macOS")]
public async Task<Process> RunDotnet(
ProcessArgs args,
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null,
IReadOnlyDictionary<string, string>? envVars = null,
bool waitForExit = true
)
{
var process = ProcessRunner.StartAnsiProcess(
DotnetPath,
args,
workingDirectory,
onProcessOutput,
envVars
);
if (!waitForExit)
return process;
await process.WaitForExitAsync();
if (process.ExitCode == 0)
return process;
Logger.Error(
"dotnet8 with args [{Args}] failed with exit code " + "{ExitCode}:\n{StdOut}\n{StdErr}",
args,
process.ExitCode,
process.StandardOutput,
process.StandardError
);
throw new ProcessException(
$"dotnet8 with args [{args}] failed with exit code"
+ $" {process.ExitCode}:\n{process.StandardOutput}\n{process.StandardError}"
);
}
[SupportedOSPlatform("Linux")]
[SupportedOSPlatform("macOS")]
public async Task InstallNodeIfNecessary(IProgress<ProgressReport>? progress = null)
{
throw new NotImplementedException();
if (IsNodeInstalled)
{
Logger.Info("node already installed");
return;
}
Logger.Info("Downloading node");
var downloadUrl = Compat.IsMacOS
? "https://nodejs.org/dist/v20.11.0/node-v20.11.0-darwin-arm64.tar.gz"
: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.gz";
var nodeDownloadPath = AssetsDir.JoinFile(Path.GetFileName(downloadUrl));
await downloadService.DownloadToFileAsync(downloadUrl, nodeDownloadPath, progress: progress);
Logger.Info("Installing node");
progress?.Report(
new ProgressReport(
progress: 0.5f,
isIndeterminate: true,
type: ProgressType.Generic,
message: "Installing prerequisites..."
)
);
// unzip
await ArchiveHelper.Extract7ZAuto(nodeDownloadPath, AssetsDir);
var nodeDir = Compat.IsMacOS
? AssetsDir.JoinDir("node-v20.11.0-darwin-arm64")
: AssetsDir.JoinDir("node-v20.11.0-linux-x64");
Directory.Move(nodeDir, NodeDir);
progress?.Report(
new ProgressReport(progress: 1f, message: "Node install complete", type: ProgressType.Generic)
);
File.Delete(nodeDownloadPath);
}
private async Task DownloadAndExtractPrerequisite(
IProgress<ProgressReport>? progress,
string downloadUrl,
string extractPath
)
{
Logger.Info($"Downloading {downloadUrl}");
var downloadPath = AssetsDir.JoinFile(Path.GetFileName(downloadUrl));
await downloadService.DownloadToFileAsync(downloadUrl, downloadPath, progress: progress);
Logger.Info("Installing prereq");
progress?.Report(
new ProgressReport(
progress: 0.5f,
isIndeterminate: true,
type: ProgressType.Generic,
message: "Installing prerequisites..."
)
);
Directory.CreateDirectory(extractPath);
// unzip
await ArchiveHelper.Extract7ZAuto(downloadPath, extractPath);
progress?.Report(
new ProgressReport(progress: 1f, message: "Node install complete", type: ProgressType.Generic)
);
File.Delete(downloadPath);
}
[UnsupportedOSPlatform("Linux")]

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

Loading…
Cancel
Save